wolfhece 2.2.20__py3-none-any.whl → 2.2.23__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.
Files changed (58) hide show
  1. wolfhece/PyDraw.py +127 -0
  2. wolfhece/__init__.py +1 -0
  3. wolfhece/_add_path.py +9 -0
  4. wolfhece/apps/check_install.py +6 -6
  5. wolfhece/apps/splashscreen.py +1 -1
  6. wolfhece/apps/version.py +1 -1
  7. wolfhece/cli.py +96 -1
  8. wolfhece/hydrology/Optimisation.py +30 -25
  9. wolfhece/report/pdf.py +55 -0
  10. wolfhece/report/simplesimgpu.py +1409 -0
  11. wolfhece/tools2d_dll.py +7 -2
  12. wolfhece/wolf_array.py +23 -5
  13. wolfhece/wolf_hist.py +21 -16
  14. wolfhece/wolfresults_2D.py +0 -24
  15. {wolfhece-2.2.20.dist-info → wolfhece-2.2.23.dist-info}/METADATA +3 -1
  16. {wolfhece-2.2.20.dist-info → wolfhece-2.2.23.dist-info}/RECORD +19 -56
  17. {wolfhece-2.2.20.dist-info → wolfhece-2.2.23.dist-info}/entry_points.txt +3 -0
  18. wolfhece/libs/GL/gl.h +0 -1044
  19. wolfhece/libs/GL/glaux.h +0 -272
  20. wolfhece/libs/GL/glcorearb.h +0 -3597
  21. wolfhece/libs/GL/glext.h +0 -11771
  22. wolfhece/libs/GL/glu.h +0 -255
  23. wolfhece/libs/GL/glxext.h +0 -926
  24. wolfhece/libs/GL/wglext.h +0 -840
  25. wolfhece/libs/MSVCP140.dll +0 -0
  26. wolfhece/libs/WolfDll.dll +0 -0
  27. wolfhece/libs/Wolf_tools.dll +0 -0
  28. wolfhece/libs/api-ms-win-crt-heap-l1-1-0.dll +0 -0
  29. wolfhece/libs/api-ms-win-crt-math-l1-1-0.dll +0 -0
  30. wolfhece/libs/api-ms-win-crt-runtime-l1-1-0.dll +0 -0
  31. wolfhece/libs/fribidi-0.dll +0 -0
  32. wolfhece/libs/get_infos.cp310-win_amd64.pyd +0 -0
  33. wolfhece/libs/get_infos.cp311-win_amd64.pyd +0 -0
  34. wolfhece/libs/get_infos.cp312-win_amd64.pyd +0 -0
  35. wolfhece/libs/get_infos.cp313-win_amd64.pyd +0 -0
  36. wolfhece/libs/glu32.dll +0 -0
  37. wolfhece/libs/hdf5.dll +0 -0
  38. wolfhece/libs/hdf5_hl.dll +0 -0
  39. wolfhece/libs/libcurl.dll +0 -0
  40. wolfhece/libs/libpardiso600-WIN-X86-64.dll +0 -0
  41. wolfhece/libs/libraqm.dll +0 -0
  42. wolfhece/libs/msvcr100.dll +0 -0
  43. wolfhece/libs/netcdf.dll +0 -0
  44. wolfhece/libs/paho-mqtt3cs.dll +0 -0
  45. wolfhece/libs/vcomp100.dll +0 -0
  46. wolfhece/libs/vcruntime140.dll +0 -0
  47. wolfhece/libs/vcruntime140_1.dll +0 -0
  48. wolfhece/libs/verify_wolf.cp310-win_amd64.pyd +0 -0
  49. wolfhece/libs/verify_wolf.cp311-win_amd64.pyd +0 -0
  50. wolfhece/libs/verify_wolf.cp312-win_amd64.pyd +0 -0
  51. wolfhece/libs/verify_wolf.cp313-win_amd64.pyd +0 -0
  52. wolfhece/libs/wolfogl.cp310-win_amd64.pyd +0 -0
  53. wolfhece/libs/wolfogl.cp311-win_amd64.pyd +0 -0
  54. wolfhece/libs/wolfogl.cp312-win_amd64.pyd +0 -0
  55. wolfhece/libs/wolfogl.cp313-win_amd64.pyd +0 -0
  56. wolfhece/libs/zlib1.dll +0 -0
  57. {wolfhece-2.2.20.dist-info → wolfhece-2.2.23.dist-info}/WHEEL +0 -0
  58. {wolfhece-2.2.20.dist-info → wolfhece-2.2.23.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1409 @@
1
+ """ Create a report on a simple simulation from wolfgpu """
2
+
3
+ import sys
4
+ import wx
5
+ import os
6
+ import platform
7
+ from pathlib import Path
8
+ import logging
9
+ import numpy as np
10
+ from tempfile import NamedTemporaryFile
11
+ from datetime import datetime as dt
12
+
13
+ import matplotlib.pyplot as plt
14
+ import seaborn as sns
15
+
16
+ import pymupdf as pdf
17
+ from wolfgpu.simple_simulation import SimpleSimulation, TimeStepStrategy, InfiltrationInterpolation
18
+ from wolfgpu.simple_simulation import InfiltrationChronology, SimulationDuration, SimulationDurationType
19
+ from wolfgpu.simple_simulation import boundary_condition_2D, BoundaryConditionsTypes
20
+
21
+ from .pdf import PDFViewer
22
+ from ..wolf_array import WolfArray, header_wolf
23
+ from ..PyTranslate import _
24
+ from .. import __version__ as wolfhece_version
25
+ from wolfgpu.version import __version__ as wolfgpu_version
26
+
27
+ def cm2pts(cm):
28
+ """ Convert centimeters to points for PyMuPDF.
29
+
30
+ One point equals 1/72 inches.
31
+ """
32
+ return cm * 28.346456692913385 # 1 cm = 28.346456692913385 points = 72/2.54
33
+
34
+ def A4_rect():
35
+ """ Return the A4 rectangle in PyMuPDF units.
36
+
37
+ (0, 0) is the top-left corner in PyMuPDF coordinates.
38
+ """
39
+ return pdf.Rect(0, 0, cm2pts(21), cm2pts(29.7)) # A4 size in points (PDF units)
40
+
41
+ def rect_cm(x, y, width, height):
42
+ """ Create a rectangle in PyMuPDF units from centimeters.
43
+
44
+ (0, 0) is the top-left corner in PyMuPDF coordinates.
45
+ """
46
+ return pdf.Rect(cm2pts(x), cm2pts(y), cm2pts(x) + cm2pts(width), cm2pts(y) + cm2pts(height))
47
+
48
+ def get_rect_from_text(text, width, fontsize=10, padding=5):
49
+ """ Get a rectangle that fits the text in PyMuPDF units.
50
+
51
+ :param text: The text to fit in the rectangle.
52
+ :param width: The width of the rectangle in centimeters.
53
+ :param fontsize: The font size in points.
54
+ :param padding: Padding around the text in points.
55
+ :return: A PyMuPDF rectangle that fits the text.
56
+ """
57
+ # Create a temporary PDF document to measure the text size
58
+ with NamedTemporaryFile(delete=True, suffix='.pdf') as temp_pdf:
59
+ doc = pdf.Document()
60
+ page = doc.new_page(A4_rect())
61
+ text_rect = page.insert_text((0, 0), text, fontsize=fontsize, width=cm2pts(width))
62
+ doc.save(temp_pdf.name)
63
+
64
+ # Get the size of the text rectangle
65
+ text_width = text_rect.width + padding * 2
66
+ text_height = text_rect.height + padding * 2
67
+ # Create a rectangle with the specified width and height
68
+ rect = pdf.Rect(0, 0, cm2pts(width), text_height)
69
+ # Adjust the rectangle to fit the text
70
+ rect.x0 -= padding
71
+ rect.y0 -= padding
72
+ rect.x1 += padding
73
+ rect.y1 += padding
74
+ return rect
75
+
76
+
77
+ def list_to_html(list_items, font_size="10pt", font_family="Helvetica"):
78
+ # Génère le CSS
79
+ css = f"""
80
+ ul.custom-list {{
81
+ font-size: {font_size};
82
+ font-family: {font_family};
83
+ color: #2C3E50;
84
+ padding-left: 20px;
85
+ }}
86
+ li {{
87
+ margin-bottom: 5px;
88
+ }}
89
+ """
90
+
91
+ # Génère le HTML
92
+ html = "<ul class='custom-list'>\n"
93
+ for item in list_items:
94
+ html += f" <li>{item}</li>\n"
95
+ html += "</ul>"
96
+
97
+ return html, css
98
+
99
+
100
+ def list_to_html_aligned(list_items, font_size="10pt", font_family="Helvetica"):
101
+ # Génère le CSS
102
+ css = f"""
103
+ ul.custom-list {{
104
+ font-size: {font_size};
105
+ font-family: {font_family};
106
+ color: #2C3E50;
107
+ padding-left: 20px;
108
+ }}
109
+ li {{
110
+ margin-bottom: 5px;
111
+ }}
112
+ """
113
+
114
+ # Génère le HTML
115
+ html = "<div class='custom-list'>\n"
116
+ html = " - ".join(list_items) # Join the items with a hyphen
117
+ html += "</div>"
118
+
119
+ return html, css
120
+
121
+
122
+ class SimpleSimGPU_Report():
123
+
124
+ def __init__(self, sim:SimpleSimulation | Path | str, **kwargs):
125
+ """ Initialize the Simple Simulation GPU Report Viewer """
126
+
127
+ self._doc = None
128
+
129
+ if isinstance(sim, Path):
130
+ try:
131
+ self._sim = SimpleSimulation.load(sim)
132
+ except Exception as e:
133
+ logging.error(f"Failed to load simulation from path {sim}: {e}")
134
+ self._sim = None
135
+ return
136
+ elif isinstance(sim, str):
137
+ try:
138
+ self._sim = SimpleSimulation.load(Path(sim))
139
+ except Exception as e:
140
+ logging.error(f"Failed to load simulation from string path {sim}: {e}")
141
+ self._sim = None
142
+ return
143
+ elif not isinstance(sim, SimpleSimulation):
144
+ try:
145
+ self._sim = sim
146
+ except Exception as e:
147
+ logging.error(f"Failed to set simulation: {e}")
148
+ self._sim = None
149
+ return
150
+ else:
151
+ logging.error("Invalid type for simulation. Must be SimpleSimulation, Path, or str.")
152
+ return
153
+
154
+ self._summary = {}
155
+ self._summary['warnings'] = self._summary_warnings()
156
+ self._summary['errors'] = self._summary_errors()
157
+
158
+ def _summary_versions(self):
159
+ """ Find the versions of the simulation, wolfhece and the wolfgpu package """
160
+ import json
161
+
162
+ sim = self._sim
163
+
164
+ with open(sim.path / "parameters.json","r") as pfile:
165
+ data = json.loads(pfile.read())
166
+ spec_version = data["spec_version"]
167
+
168
+ group_title = "Versions"
169
+ text = [f"Simulation : {spec_version}",
170
+ f"Wolfhece : {wolfhece_version}",
171
+ f"Wolfgpu : {wolfgpu_version}",
172
+ f"Python : {sys.version.split()[0]}",
173
+ f"Operating System: {os.name}"
174
+ ]
175
+
176
+ return group_title, text
177
+
178
+ def _summary_spatial_extent(self):
179
+ """ Return the summary of the spatial extent of the simulation """
180
+ sim = self._sim
181
+
182
+ group_title = "Spatial Extent"
183
+ text = [f"Lower-left corner [m LBT72] : ({sim.param_base_coord_ll_x}, {sim.param_base_coord_ll_y})",
184
+ f"Upper-right corner [m LBT72] : ({sim.param_base_coord_ll_x + sim.param_dx * sim.param_nx}, {sim.param_base_coord_ll_y + sim.param_dy * sim.param_ny})",
185
+ f"Resolution [m] : ({sim.param_dx}, {sim.param_dy}) - {sim.param_dx * sim.param_dy} [m²]",
186
+ f"Total Area : {sim.param_dx * sim.param_dy * np.count_nonzero(sim.nap == 1)} [m²] - {sim.param_dx * sim.param_dy * np.count_nonzero(sim.nap == 1) /1e6} [km²]",
187
+ ]
188
+
189
+ return group_title, text
190
+
191
+ def _summary_number_of_cells(self):
192
+ """ Return the summary of the number of cells in the simulation """
193
+ sim = self._sim
194
+
195
+ group_title = "Number of Cells"
196
+ text = [f"X \u2192: {sim.param_nx}",
197
+ f"Y \u2191: {sim.param_ny}",
198
+ f"Total in NAP: {np.count_nonzero(sim.nap == 1)}",
199
+ f"Total in Bathymetry: {np.count_nonzero(sim.bathymetry != 99999.)}",
200
+ ]
201
+
202
+
203
+ return group_title, text
204
+
205
+ def _summary_time_evolution(self):
206
+ """ Return the summary of the time evolution of the simulation """
207
+ sim = self._sim
208
+
209
+ group_title = "Time Evolution"
210
+ text = []
211
+ if sim.param_runge_kutta == 1.:
212
+ text.append("Euler explicit 1st order scheme")
213
+ elif sim.param_runge_kutta == 0.5:
214
+ text.append("Runge-Kutta 2nd order scheme (RK22)")
215
+ else:
216
+ text.append(f"Runge-Kutta 1st order scheme (RK21) - {sim.param_runge_kutta} times predictor")
217
+
218
+ if sim.param_timestep_strategy == TimeStepStrategy.FIXED_TIME_STEP:
219
+ text.append(f"Fixed time step: {sim.param_timestep} seconds")
220
+ elif sim.param_timestep_strategy == TimeStepStrategy.OPTIMIZED_TIME_STEP:
221
+ text.append("Variable time step")
222
+ text.append(f"Courant-Friedrichs-Lewy condition: {sim.param_courant}")
223
+
224
+ text.append(f"Simulation duration: {sim.param_duration}")
225
+
226
+ return group_title, text
227
+
228
+ def _summary_boundary_conditions(self):
229
+ """ Return the summary of the boundary conditions of the simulation """
230
+ sim = self._sim
231
+
232
+ group_title = "Boundary Conditions"
233
+ text = [f"Count: {len(sim.boundary_condition)}"]
234
+
235
+ bc_set = {}
236
+ for bc in sim.boundary_condition:
237
+ if bc.ntype not in bc_set:
238
+ bc_set[bc.ntype] = 0
239
+ bc_set[bc.ntype] += 1
240
+
241
+ for bc_type, count in bc_set.items():
242
+ if count > 0:
243
+ text.append(f"{count} {bc_type.name}")
244
+
245
+ if BoundaryConditionsTypes.FROUDE_NORMAL in bc_set:
246
+ if sim.param_froude_bc_limit_tolerance == 1.0:
247
+ text.append("Froude tolerance is set to 1.0, which can lead to supercritical flow.")
248
+ elif sim.param_froude_bc_limit_tolerance < 1.0:
249
+ text.append(f"Froude tolerance is set to {sim.param_froude_bc_limit_tolerance}, which is a BAD practice.")
250
+ else:
251
+ text.append(f"Froude tolerance is set to {sim.param_froude_bc_limit_tolerance}, which is a GOOD practice.")
252
+
253
+ return group_title, text
254
+
255
+ def _summary_infiltration(self):
256
+ """ Return the summary of the infiltration conditions of the simulation """
257
+ sim = self._sim
258
+
259
+ group_title = "Infiltration Conditions"
260
+ text = []
261
+
262
+ if len(sim.infiltrations_chronology) == 0:
263
+ text.append("No infiltration conditions defined.")
264
+ return group_title, text
265
+
266
+ text.append(f"Count: {len(sim.infiltrations_chronology)}")
267
+ text.append(f"Interpolation method: {sim.param_infiltration_lerp.name}")
268
+
269
+ text.append(f"Rows - number of time positions: {len(sim.infiltrations_chronology)}")
270
+ text.append(f"Columns - number of infiltration zones: {len(sim.infiltrations_chronology[0][1])}")
271
+ text.append(f"Starting time [s]: {sim.infiltrations_chronology[0][0]}")
272
+ text.append(f"Ending time [s]: {sim.infiltrations_chronology[-1][0]}")
273
+ text.append(f"Ending time [hour]: {sim.infiltrations_chronology[-1][0] / 3600.}")
274
+
275
+ nb_mat = np.max(sim.infiltration_zones)
276
+ if nb_mat != len(sim.infiltrations_chronology[0][1]):
277
+ text.append('PROBLEM: The number of infiltration zones in the chronology does not match the number of zones in the simulation.')
278
+
279
+ # Count the cells for each infiltration zone
280
+ nb_cells = np.bincount(sim.infiltration_zones[sim.infiltration_zones != 0], minlength=nb_mat + 1)
281
+ # text.append("Number of cells per infiltration zone:")
282
+ for i in range(1, nb_mat + 1):
283
+ text.append(f"Zone {i}: {nb_cells[i]} cells")
284
+
285
+ return group_title, text
286
+
287
+ def _figure_infiltration(self):
288
+ """ Add the infiltration image to the PDF report """
289
+
290
+ sim = self._sim
291
+ fig, ax = sim.plot_infiltration(toshow= False)
292
+ # set font size
293
+ ax.tick_params(axis='both', which='major', labelsize=6)
294
+ for label in ax.get_xticklabels():
295
+ label.set_fontsize(6)
296
+ for label in ax.get_yticklabels():
297
+ label.set_fontsize(6)
298
+ # and gfor the label title
299
+ ax.set_xlabel(ax.get_xlabel(), fontsize=8)
300
+ ax.set_ylabel(ax.get_ylabel(), fontsize=8)
301
+ # and for the legend
302
+ for label in ax.get_legend().get_texts():
303
+ label.set_fontsize(6)
304
+ # remove the titla
305
+ ax.set_title('')
306
+ fig.suptitle('')
307
+
308
+ fig.set_size_inches(8, 6)
309
+ fig.tight_layout()
310
+
311
+ ax.set_ylabel('Total Q\n[$m^3/s$]')
312
+
313
+ return fig
314
+
315
+ def _summary_bathymetry(self):
316
+ """ Return the summary of the bathymetry of the simulation """
317
+ sim = self._sim
318
+
319
+ group_title = "Bathymetry"
320
+ text = []
321
+
322
+ if sim.bathymetry is None:
323
+ text.append("No bathymetry defined.")
324
+ return group_title, text
325
+
326
+ text.append(f"NoData value: {sim.bathymetry[0, 0]}")
327
+ if sim.bathymetry[0, 0] != 99999.:
328
+ text.append("Nodata value is not 99999. It is preferable to use this value.")
329
+
330
+ np_bath = np.count_nonzero(sim.bathymetry[sim.bathymetry != sim.bathymetry[0, 0]])
331
+ np_nap = np.count_nonzero(sim.nap[sim.nap == 1])
332
+ text.append(f"Number of cells in bathymetry: {np.count_nonzero(sim.bathymetry[sim.bathymetry != sim.bathymetry[0,0]])}")
333
+ if np_nap != np_bath:
334
+ text.append(f"Number of cells in NAP: {np_nap} is not equal to the number of cells in the bathymetry.")
335
+ text.append(f"Minimum bathymetry value: {np.min(sim.bathymetry[sim.bathymetry != 99999.]):.3f} m")
336
+ text.append(f"Maximum bathymetry value: {np.max(sim.bathymetry[sim.bathymetry != 99999.]):.3f} m")
337
+
338
+ return group_title, text
339
+
340
+ def _summary_initial_conditions(self):
341
+ """ Return the summary of the initial conditions of the simulation """
342
+ sim = self._sim
343
+
344
+ group_title = "Initial Conditions"
345
+ text = []
346
+
347
+ max_h = np.max(sim.h)
348
+ max_qx = np.max(np.abs(sim.qx))
349
+ max_qy = np.max(np.abs(sim.qy))
350
+
351
+ if max_h == 0.:
352
+ text.append("No initial conditions defined. All cells are set to 0.")
353
+ if max_qx != 0. or max_qy != 0.:
354
+ text.append("Warning: Initial conditions for qx and qy are not zero, which is unusual.")
355
+ else:
356
+ text.append(f"Maximum water depth: {max_h} m")
357
+ text.append(f"Number of wetted cells: {np.count_nonzero(sim.h > 0.)}")
358
+ text.append(f"Maximum |qx|: {max_qx} m^2/s")
359
+ text.append(f"Maximum |qy|: {max_qy} m^2/s")
360
+
361
+ return group_title, text
362
+
363
+ def _figure_histogram_waterdepth(self):
364
+ """ Add the histogram of bathymetry to the PDF report """
365
+ sim = self._sim
366
+ # Plot the histogram of waterdepth adn add it to the PDF
367
+ fig, ax = plt.subplots(figsize=(8, 6))
368
+ # Plot the histogram of water depth
369
+ ax.hist(sim.h[sim.h > 0.], bins=100, density=True)
370
+ # ax.set_title('Histogram of Water Depth')
371
+ ax.set_xlim(0, np.max(sim.h[sim.h > 0.]) * 1.25) # Set xlim to 110% of max value
372
+ ax.set_xlabel('Water Depth [m]')
373
+ ax.set_ylabel('Frequency')
374
+
375
+ # set font size of the labels
376
+ ax.tick_params(axis='both', which='major', labelsize=6)
377
+ for label in ax.get_xticklabels():
378
+ label.set_fontsize(6)
379
+ for label in ax.get_yticklabels():
380
+ label.set_fontsize(6)
381
+ # and gfor the label title
382
+ ax.set_xlabel(ax.get_xlabel(), fontsize=8)
383
+ ax.set_ylabel(ax.get_ylabel(), fontsize=8)
384
+
385
+ fig.tight_layout()
386
+
387
+ return fig
388
+
389
+ def _figure_histogram_manning(self):
390
+ """ Add the histogram of bathymetry to the PDF report """
391
+ sim = self._sim
392
+ # Plot the histogram of waterdepth adn add it to the PDF
393
+ fig, ax = plt.subplots(figsize=(8, 6))
394
+ # set font size
395
+ ax.hist(sim.manning[sim.nap == 1], bins=100, density = True)
396
+ # ax.set_title('Histogram of Manning Coefficient')
397
+ ax.set_xlabel('Manning [$\\frac {s} {m^{1/3}} $]')
398
+ ax.set_xlim(0, np.max(sim.manning[sim.nap == 1]) * 1.25) # Set xlim to 110% of max value
399
+ ax.set_ylabel('Frequency')
400
+
401
+ # set font size of the labels
402
+ ax.tick_params(axis='both', which='major', labelsize=6)
403
+ for label in ax.get_xticklabels():
404
+ label.set_fontsize(6)
405
+ for label in ax.get_yticklabels():
406
+ label.set_fontsize(6)
407
+ # and gfor the label title
408
+ ax.set_xlabel(ax.get_xlabel(), fontsize=8)
409
+ ax.set_ylabel(ax.get_ylabel(), fontsize=8)
410
+
411
+ fig.tight_layout()
412
+
413
+ return fig
414
+
415
+ def _summary_bridge(self):
416
+ """ Return the summary of the bridge conditions of the simulation """
417
+ sim = self._sim
418
+
419
+ group_title = "Bridge"
420
+ text = []
421
+
422
+ if not sim.has_bridge():
423
+ text.append("No bridge defined.")
424
+ return group_title, text
425
+
426
+ text.append("Bridge defined.")
427
+ if sim.bridge_roof is not None:
428
+ text.append(f"Number of cells: {np.count_nonzero(sim.bridge_roof != 99999.)}")
429
+ text.append(f"Minimum bridge roof value: {np.min(sim.bridge_roof[sim.bridge_roof != 99999.]):.3f} m")
430
+ text.append(f"Maximum bridge roof value: {np.max(sim.bridge_roof[sim.bridge_roof != 99999.]):.3f} m")
431
+ else:
432
+ text.append("No bridge roof defined.")
433
+
434
+ return group_title, text
435
+
436
+ def _summary_warnings(self):
437
+ """ Return the summary of the warnings of the simulation """
438
+ sim = self._sim
439
+
440
+ group_title = "Warnings"
441
+ text = []
442
+
443
+ mann = np.unique(sim.manning[sim.nap == 1])
444
+
445
+ if len(mann) == 0:
446
+ text.append("No Manning coefficient defined.")
447
+ elif len(mann) == 1 and mann[0] == 0.:
448
+ text.append("No Manning coefficient defined. All cells are set to 0.")
449
+ elif len(mann) == 1 and mann[0] < 0.:
450
+ text.append(f"Warning: Manning coefficient is set to {mann[0]:.4f} which is negative. This is not a valid value.")
451
+ elif len(mann) == 1 and mann[0] > 0.:
452
+ text.append(f"Manning coefficient is set to {mann[0]:.4f} which is a valid value BUT uniform.")
453
+
454
+ h = np.unique(sim.h[sim.nap == 1])
455
+ if len(h) == 0:
456
+ text.append("No water depth defined. All cells are set to 0.")
457
+ elif len(h) == 1 and h[0] == 0.:
458
+ text.append("No water depth defined. All cells are set to 0.")
459
+ elif len(h) == 1 and h[0] < 0.:
460
+ text.append(f"Warning: Water depth is set to {h[0]:.2f} which is negative. This is not a valid value.")
461
+ elif (len(h) == 1 and h[0] > 0.) or (len(h) == 2 and h[0] == 0. and h[1] > 0.):
462
+ text.append(f"Water depth is set to {h[0]:.4f} which is a valid value BUT uniform.")
463
+
464
+ wsl = np.unique(sim.h[sim.h > 0.] + sim.bathymetry[sim.h >0.])
465
+ if len(wsl) == 1 and h[0] > 0.:
466
+ text.append(f"Water surface level is set to {wsl[0]:.2f} which is a valid value BUT uniform.")
467
+
468
+ qx = np.unique(sim.qx[sim.nap == 1])
469
+ if len(qx) == 0:
470
+ text.append("No initial conditions for qx defined. All cells are set to 0.")
471
+ elif len(qx) == 1 and qx[0] == 0.:
472
+ text.append("No initial conditions for qx defined. All cells are set to 0.")
473
+ elif len(qx) == 1 and qx[0] < 0.:
474
+ text.append(f"Warning: Initial conditions for qx is set to {qx[0]:.2f} which is negative. This is not a valid value.")
475
+ elif len(qx) == 1 and qx[0] > 0.:
476
+ text.append(f"Initial conditions for qx is set to {qx[0]:.2f} which is a valid value BUT uniform.")
477
+
478
+ qy = np.unique(sim.qy[sim.nap == 1])
479
+ if len(qy) == 0:
480
+ text.append("No initial conditions for qy defined. All cells are set to 0.")
481
+ elif len(qy) == 1 and qy[0] == 0.:
482
+ text.append("No initial conditions for qy defined. All cells are set to 0.")
483
+ elif len(qy) == 1 and qy[0] < 0.:
484
+ text.append(f"Warning: Initial conditions for qy is set to {qy[0]:.2f} which is negative. This is not a valid value.")
485
+ elif len(qy) == 1 and qy[0] > 0.:
486
+ text.append(f"Initial conditions for qy is set to {qy[0]:.2f} which is a valid value BUT uniform.")
487
+
488
+ # test the presence of simul_gpu_results directory
489
+ if not (sim.path / "simul_gpu_results").exists():
490
+ text.append("No 'simul_gpu_results' directory found. The simulation may not have been run or the results are missing.")
491
+ elif not (sim.path / "simul_gpu_results" / "metadata.json").exists():
492
+ text.append("No 'metadata.json' file found in 'simul_gpu_results'. The simulation may not have been run or the results are missing.")
493
+ else:
494
+ # Check the date of the metadata file compared to the parameters.json file
495
+ metadata_file = sim.path / "simul_gpu_results" / "metadata.json"
496
+ parameters_file = sim.path / "parameters.json"
497
+ metadata_date = dt.fromtimestamp(metadata_file.stat().st_mtime)
498
+ parameters_date = dt.fromtimestamp(parameters_file.stat().st_mtime)
499
+ if metadata_date < parameters_date:
500
+ text.append("Warning: The 'metadata.json' file is older than the 'parameters.json' file. The simulation may not have been run with the latest parameters.")
501
+
502
+ warn = sim.check_warnings()
503
+ if warn is not None:
504
+ text.append(f"Count: {len(warn)}")
505
+ for w in warn:
506
+ text.append(f"- {w}")
507
+ else:
508
+ text.append("No warnings from wolfgpu.")
509
+
510
+ return group_title, text
511
+
512
+ def _summary_errors(self):
513
+ """ Return the summary of the errors of the simulation """
514
+ sim = self._sim
515
+
516
+ group_title = "Errors"
517
+ text = []
518
+
519
+ err = sim.check_errors()
520
+ if err is not None:
521
+ text.append(f"Count: {len(err)}")
522
+ for e in err:
523
+ text.append(f"- {e}")
524
+ else:
525
+ text.append("No errors from wolfgpu.")
526
+
527
+ return group_title, text
528
+
529
+ def _figure_model_extent(self):
530
+ """ Get the bathymetry figure for the PDF report """
531
+ h = header_wolf()
532
+ h.shape = (self._sim.param_nx, self._sim.param_ny)
533
+ h.set_resolution(self._sim.param_dx, self._sim.param_dy)
534
+ h.set_origin(self._sim.param_base_coord_ll_x, self._sim.param_base_coord_ll_y)
535
+
536
+ bath = self._sim.bathymetry
537
+ bat_wa = WolfArray(srcheader=h, np_source=bath, nullvalue= 99999.)
538
+
539
+ fig, ax, im = bat_wa.plot_matplotlib(getdata_im= True,
540
+ Walonmap= True, cat= 'IMAGERIE/ORTHO_2021',
541
+ with_legend= False)
542
+
543
+ # set font size of the labels
544
+ ax.tick_params(axis='both', which='major', labelsize=6)
545
+ for label in ax.get_xticklabels():
546
+ label.set_fontsize(6)
547
+ for label in ax.get_yticklabels():
548
+ label.set_fontsize(6)
549
+
550
+ return fig
551
+
552
+ def summary(self):
553
+ """ Create a dictionnary with the summary of the simulation """
554
+
555
+ self._summary['versions'] = self._summary_versions()
556
+ self._summary['spatial_extent'] = self._summary_spatial_extent()
557
+ self._summary['number_of_cells'] = self._summary_number_of_cells()
558
+ self._summary['time_evolution'] = self._summary_time_evolution()
559
+ self._summary['boundary_conditions'] = self._summary_boundary_conditions()
560
+ self._summary['infiltration'] = self._summary_infiltration()
561
+ self._summary['bathymetry'] = self._summary_bathymetry()
562
+ self._summary['initial_conditions'] = self._summary_initial_conditions()
563
+ self._summary['bridge'] = self._summary_bridge()
564
+ self._summary['warnings'] = self._summary_warnings()
565
+ self._summary['errors'] = self._summary_errors()
566
+
567
+ return self._summary
568
+
569
+ def _layout(self):
570
+ """ Set the layout of the PDF report.
571
+
572
+ Each group has a rectangle of 9cm width and 2.5 cm height.
573
+
574
+ Title rect is 16 cm width and 1.5 cm height.
575
+ Version rect is 16 cm width and 1 cm height.
576
+ Logo is at the top-right corner of the page (2 cm width x 3 cm height).
577
+ """
578
+
579
+ summary = self.summary()
580
+
581
+ LEFT_MARGIN = 1 # cm
582
+ TOP_MARGIN = 0.5 # cm
583
+ PADDING = 0.5 # cm
584
+
585
+ WIDTH_TITLE = 16 # cm
586
+ HEIGHT_TITLE = 1.5 # cm
587
+
588
+ WIDTH_VERSIONS = 16 # cm
589
+ HEIGHT_VERSIONS = .5 # cm
590
+
591
+ X_LOGO = 18.5 # Logo starts after the title and versions
592
+ WIDTH_LOGO = 1.5 # cm
593
+ HEIGHT_LOGO = 1.5 # cm
594
+
595
+ WIDTH_SUMMARY = 9 # cm
596
+ HEIGHT_SUMMARY = 2.5 # cm
597
+
598
+ KEYS_LEFT_COL = ["warnings", "spatial_extent", "number_of_cells",
599
+ "time_evolution", "boundary_conditions",
600
+ "infiltration", "bathymetry",
601
+ "initial_conditions"]
602
+ KEYS_RIGHT_COL = ["errors", "bridge"]
603
+
604
+ layout = {}
605
+
606
+ layout['title'] = rect_cm(LEFT_MARGIN, TOP_MARGIN, WIDTH_TITLE, HEIGHT_TITLE)
607
+ layout['versions'] = rect_cm(LEFT_MARGIN, TOP_MARGIN + HEIGHT_TITLE + PADDING, WIDTH_VERSIONS, HEIGHT_VERSIONS)
608
+ layout['logo'] = rect_cm(X_LOGO, TOP_MARGIN, WIDTH_LOGO, HEIGHT_LOGO)
609
+
610
+ TOP_SUMMARY = TOP_MARGIN + HEIGHT_TITLE + HEIGHT_VERSIONS + 2 * PADDING # 1.5 cm for title, 1 cm for versions, 0.5 cm padding
611
+
612
+ y_summary_left = TOP_SUMMARY
613
+ y_summary_right = y_summary_left # 6 groups in the left column
614
+
615
+ X_SUMMARY_RIGHT = LEFT_MARGIN + WIDTH_SUMMARY + 2 * PADDING # Right column starts after the left column
616
+
617
+ HEIGHT_MAIN_FIGURE = 3 * HEIGHT_SUMMARY + 2 * PADDING # cm
618
+ WIDTH_MAIN_FIGURE = WIDTH_SUMMARY #/ 2 # cm
619
+
620
+ # y_summary_right += 1.5 * HEIGHT_SUMMARY + 1.5* PADDING # Move the right column down for the histogram
621
+
622
+ for key, text in summary.items():
623
+ if key in KEYS_LEFT_COL:
624
+ layout[key] = rect_cm(LEFT_MARGIN, y_summary_left, WIDTH_SUMMARY, HEIGHT_SUMMARY)
625
+ y_summary_left += HEIGHT_SUMMARY + PADDING
626
+ elif key in KEYS_RIGHT_COL:
627
+ layout[key] = rect_cm(X_SUMMARY_RIGHT, y_summary_right, WIDTH_SUMMARY, HEIGHT_SUMMARY)
628
+ y_summary_right += HEIGHT_SUMMARY + PADDING
629
+
630
+ layout['main figure'] = rect_cm(X_SUMMARY_RIGHT, y_summary_right, WIDTH_MAIN_FIGURE, HEIGHT_MAIN_FIGURE) # Main figure at the bottom
631
+ # layout['manning figure'] = rect_cm(X_SUMMARY_RIGHT + WIDTH_MAIN_FIGURE + PADDING, y_summary_right, WIDTH_MAIN_FIGURE, HEIGHT_MAIN_FIGURE) # Main figure at the bottom
632
+ y_summary_right += HEIGHT_MAIN_FIGURE + PADDING # Move the right column down for the main figure
633
+
634
+ layout['hydrograms'] = rect_cm(X_SUMMARY_RIGHT, y_summary_right, WIDTH_SUMMARY, 1.5 * HEIGHT_SUMMARY + PADDING) # Hydrograms at the bottom right
635
+ y_summary_right += 1.5 * HEIGHT_SUMMARY + 1.5 * PADDING # Move the right column down for the hydrograms
636
+
637
+ layout['histogram_waterdepth'] = rect_cm(X_SUMMARY_RIGHT, y_summary_right, WIDTH_SUMMARY /2., 1.5 * HEIGHT_SUMMARY + PADDING) # Histogram below hydrograms
638
+ layout['histogram_manning'] = rect_cm(X_SUMMARY_RIGHT + WIDTH_SUMMARY/2. + PADDING, y_summary_right, WIDTH_SUMMARY /2., 1.5 * HEIGHT_SUMMARY + PADDING) # Histogram below hydrograms
639
+
640
+ layout['footer'] = rect_cm(LEFT_MARGIN, 28, 19, 1.2) # Footer at the bottom
641
+
642
+ return layout, summary
643
+
644
+ def create_report(self):
645
+ """ Create the PDF report for the Simple Simulation GPU """
646
+
647
+ if not self._sim:
648
+ logging.error("No simulation data available to create report.")
649
+ return
650
+
651
+ # Create a new PDF document
652
+ self._doc = pdf.Document()
653
+
654
+ # Add a page
655
+ page = self._doc.new_page()
656
+
657
+ layout, summary = self._layout()
658
+
659
+ page.insert_htmlbox(layout['title'], f"<h1>GPU Summary - {self._sim.path.name}</h1>",
660
+ css='h1 {font-size:16pt; font-family:Helvetica; color:#333}')
661
+
662
+ for key, text in summary.items():
663
+ rect = layout[key]
664
+
665
+ # Add a rectangle for the group
666
+ page.draw_rect(rect, color=(0, 0, 0, .05), width=0.5)
667
+
668
+
669
+ if key == "versions":
670
+ try:
671
+ html, css = list_to_html_aligned(text[1], font_size="6pt", font_family="Helvetica")
672
+ spare_height, scale = page.insert_htmlbox(rect, html, css=css,
673
+ scale_low = 0.1)
674
+
675
+ if spare_height < 0.:
676
+ logging.warning("Text overflow in versions box. Adjusting scale.")
677
+ except:
678
+ logging.error("Failed to insert versions text. Using fallback method.")
679
+
680
+ elif key == "infiltration":
681
+ # Add the group title
682
+ page.insert_text((rect.x0 + 1, rect.y0 + cm2pts(.05)), text[0],
683
+ fontsize=10, fontname="helv", fill=(0, 0, 0), fill_opacity=1.)
684
+
685
+ text_left = [txt for txt in text[1] if not txt.startswith("Zone")]
686
+ text_right = [txt for txt in text[1] if txt.startswith("Zone")]
687
+
688
+ # Limit text_right to 6 elements max
689
+ if len(text_right) > 6:
690
+ text_right = text_right[:6]
691
+ text_right.append("... (more zones)")
692
+
693
+ html_left, css = list_to_html(text_left, font_size="8pt", font_family="Helvetica")
694
+ html_right, css = list_to_html(text_right, font_size="8pt", font_family="Helvetica")
695
+
696
+ rect_left = pdf.Rect(rect.x0, rect.y0, rect.x0 + rect.width / 2, rect.y1)
697
+ rect_right = pdf.Rect(rect.x0 + rect.width / 2, rect.y0, rect.x1, rect.y1)
698
+ spare_height, scale = page.insert_htmlbox(rect_left, html_left,
699
+ scale_low = 0.1, css = css)
700
+ if spare_height < 0.:
701
+ logging.warning("Text overflow in left infiltration box. Adjusting scale.")
702
+
703
+ spare_height, scale = page.insert_htmlbox(rect_right, html_right,
704
+ scale_low = 0.1, css = css)
705
+ if spare_height < 0.:
706
+ logging.warning("Text overflow in right infiltration box. Adjusting scale.")
707
+
708
+ elif key == "warnings" or key == "errors":
709
+ # Add the group title
710
+ if key == "warnings":
711
+ page.insert_text((rect.x0 + 1, rect.y0 + cm2pts(.05)), text[0],
712
+ fontsize=10, fontname="helv", fill=(1, .55, 0), fill_opacity=1.)
713
+ if len(text[1]) > 1:
714
+ # draw rectangle in orange
715
+ page.draw_rect(rect, color=(1, .55, 0), width=2.0, fill = True, fill_opacity=1)
716
+ else:
717
+ page.insert_text((rect.x0 + 1, rect.y0 + cm2pts(.05)), text[0],
718
+ fontsize=10, fontname="helv", fill=(1, 0, 0), fill_opacity=1.)
719
+ if len(text[1]) > 1:
720
+ # draw rectangle in red
721
+ page.draw_rect(rect, color=(1, 0, 0), width=2.0, fill = True, fill_opacity=1)
722
+ try:
723
+
724
+ html, css = list_to_html(text[1], font_size="8pt", font_family="Helvetica")
725
+ spare_height, scale = page.insert_htmlbox(rect, html,
726
+ scale_low = 0.1, css = css)
727
+
728
+ if spare_height < 0.:
729
+ logging.warning("Text overflow in summary box. Adjusting scale.")
730
+ except:
731
+ logging.error("Failed to insert text. Using fallback method.")
732
+
733
+ else:
734
+ # Add the group title
735
+ page.insert_text((rect.x0 + 1, rect.y0 + cm2pts(.05)), text[0],
736
+ fontsize=10, fontname="helv", fill=(0, 0, 0), fill_opacity=1.)
737
+ try:
738
+
739
+ html, css = list_to_html(text[1], font_size="8pt", font_family="Helvetica")
740
+ spare_height, scale = page.insert_htmlbox(rect, html,
741
+ scale_low = 0.1, css = css)
742
+
743
+ if spare_height < 0.:
744
+ logging.warning("Text overflow in summary box. Adjusting scale.")
745
+ except:
746
+ logging.error("Failed to insert text. Using fallback method.")
747
+
748
+ # aded the Figures
749
+ if 'main figure' in layout:
750
+
751
+ rect = layout['main figure']
752
+
753
+ fig = self._figure_model_extent()
754
+
755
+ # set size to fit the rectangle
756
+ fig.set_size_inches(rect.width / 72, rect.height / 72)
757
+
758
+ # convert canvas to PNG and insert it into the PDF
759
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
760
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=200)
761
+ page.insert_image(layout['main figure'], filename = temp_file.name)
762
+ # delete the temporary file
763
+ temp_file.delete = True
764
+ temp_file.close()
765
+
766
+ # Force to delete fig
767
+ plt.close(fig)
768
+
769
+ if 'hydrograms' in layout:
770
+
771
+ rect = layout['hydrograms']
772
+ # Get the hydrograms figure from the simulation
773
+ fig = self._figure_infiltration()
774
+ # set size to fit the rectangle
775
+ fig.set_size_inches(rect.width / 72, rect.height / 72)
776
+
777
+ fig.tight_layout()
778
+ # convert canvas to PNG and insert it into the PDF
779
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
780
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=200)
781
+ page.insert_image(layout['hydrograms'], filename=temp_file.name)
782
+ # delete the temporary file
783
+ temp_file.delete = True
784
+ temp_file.close()
785
+
786
+ #force to delete fig
787
+ plt.close(fig)
788
+
789
+ if 'histogram_waterdepth' in layout:
790
+
791
+ rect = layout['histogram_waterdepth']
792
+ # Get the histogram of bathymetry figure from the simulation
793
+ fig = self._figure_histogram_waterdepth()
794
+ # set size to fit the rectangle
795
+ fig.set_size_inches(rect.width / 72, rect.height / 72)
796
+ fig.tight_layout()
797
+ # convert canvas to PNG and insert it into the PDF
798
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
799
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=200)
800
+ page.insert_image(layout['histogram_waterdepth'], filename=temp_file.name)
801
+ # delete the temporary file
802
+ temp_file.delete = True
803
+ temp_file.close()
804
+
805
+ # force to delete fig
806
+ plt.close(fig)
807
+
808
+ if 'histogram_manning' in layout:
809
+ rect = layout['histogram_manning']
810
+ # Get the histogram of Manning figure from the simulation
811
+ fig = self._figure_histogram_manning()
812
+ # set size to fit the rectangle
813
+ fig.set_size_inches(rect.width / 72, rect.height / 72)
814
+ fig.tight_layout()
815
+ # convert canvas to PNG and insert it into the PDF
816
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
817
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=200)
818
+ page.insert_image(layout['histogram_manning'], filename=temp_file.name)
819
+ # delete the temporary file
820
+ temp_file.delete = True
821
+ temp_file.close()
822
+
823
+ # force to delete fig
824
+ plt.close(fig)
825
+
826
+ rect = layout['logo']
827
+ # Add the logo to the top-right corner
828
+ logo_path = Path(__file__).parent.parent / 'apps' / 'WolfPython2.png'
829
+ if logo_path.exists():
830
+ page.insert_image(rect, filename=str(logo_path), keep_proportion=True,
831
+ overlay=True)
832
+
833
+ # Footer
834
+ # ------
835
+ # Insert the date and time of the report generation, the user and the PC name
836
+ footer_rect = layout['footer']
837
+ footer_text = f"<p>Report generated on {dt.now()} by {os.getlogin()} on {platform.uname().node} - {platform.uname().machine} - {platform.uname().release} - {platform.uname().version}</br> \
838
+ This report does not guarantee the quality of the simulation and in no way commits the software developers.</p>"
839
+ page.insert_htmlbox(footer_rect, footer_text,
840
+ css='p {font-size:10pt; font-family:Helvetica; color:#BEBEBE; align-text:center}',)
841
+
842
+
843
+ def save_report(self, output_path: Path | str = None):
844
+ """ Save the report to a PDF file """
845
+
846
+ # Save the PDF to a file
847
+ if output_path is None:
848
+ output_path = self._sim.path.with_suffix('.pdf')
849
+
850
+ try:
851
+ self._doc.subset_fonts()
852
+ self._doc.save(output_path, garbage=3, deflate=True)
853
+ self._pdf_path = output_path
854
+ except Exception as e:
855
+ logging.error(f"Failed to save the report to {output_path}: {e}")
856
+ logging.error("Please check if the file is already opened.")
857
+ self._pdf_path = None
858
+ return
859
+
860
+ @property
861
+ def pdf_path(self):
862
+ """ Return the PDF document """
863
+ return self._pdf_path
864
+
865
+
866
+ class SimpleSimGPU_Report_wx(PDFViewer):
867
+
868
+ def __init__(self, sim:SimpleSimulation | Path | str, **kwargs):
869
+ """ Initialize the Simple Simulation GPU Report Viewer """
870
+
871
+ super(SimpleSimGPU_Report_wx, self).__init__(None, **kwargs)
872
+
873
+ self._report = SimpleSimGPU_Report(sim, **kwargs)
874
+
875
+ if self._report._sim is None:
876
+ logging.error("No simulation data available to create report.")
877
+ return
878
+
879
+ self._report.create_report()
880
+ self._report.save_report()
881
+
882
+ if self._report.pdf_path is None:
883
+ logging.error("Failed to create the report PDF. Check the logs for more details.")
884
+ return
885
+
886
+ # Load the PDF into the viewer
887
+ self.load_pdf(self._report.pdf_path)
888
+ self.viewer.SetZoom(-1) # Fit to width
889
+
890
+ # Set the title of the frame
891
+ self.SetTitle(f"Simple Simulation GPU Report - {self._report._sim.path}")
892
+
893
+ self.Bind(wx.EVT_CLOSE, self.on_close)
894
+
895
+ def on_close(self, event):
896
+ """ Handle the close event of the frame """
897
+
898
+ # close the pdf document
899
+ self.viewer.pdfdoc.pdfdoc.close()
900
+ self.Destroy()
901
+
902
+ class SimpleSimGPU_Reports_wx():
903
+ """ List of Simple Simulations GPU """
904
+
905
+ def __init__(self, dir_or_sims:list[SimpleSimulation | Path | str] | Path, show:bool=True, **kwargs):
906
+ """ Initialize the Simple Simulations GPU Reports """
907
+
908
+ pgbar = wx.ProgressDialog("Reporting simulations",
909
+ "Creating report...",
910
+ maximum=100,
911
+ style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE | wx.PD_SMOOTH)
912
+
913
+ if isinstance(dir_or_sims, Path):
914
+ # We assume it is a directory containing SimpleSimulation files
915
+ if dir_or_sims.is_dir():
916
+ sims = list(dir_or_sims.rglob('parameters.json'))
917
+ self._sims = []
918
+ for i, sim_dir in enumerate(sims):
919
+ pgbar.Update(int((i / len(sims)) * 100), f"Loading simulation {sim_dir.parent.name} ({i+1}/{len(sims)})")
920
+ self._sims.append(SimpleSimGPU_Report_wx(sim_dir.parent, **kwargs)) # Create a report for each simulation
921
+
922
+ pgbar.Update(100, "All simulations loaded.")
923
+ pgbar.Destroy()
924
+ self._sims = [sim for sim in self._sims if sim._report._sim is not None] # Filter out None values
925
+ else:
926
+ raise ValueError(f"The path {dir_or_sims} is not a directory.")
927
+ elif isinstance(dir_or_sims, list):
928
+ self._sims = []
929
+ for sim in dir_or_sims:
930
+ pgbar.Update(int((len(self._sims) / len(dir_or_sims)) * 100), f"Loading simulation {sim} ({len(self._sims) + 1}/{len(dir_or_sims)})")
931
+ self._sims.append(SimpleSimGPU_Report_wx(sim, **kwargs))
932
+ pgbar.Update(100, "All simulations loaded.")
933
+ pgbar.Destroy()
934
+
935
+ self._sims = [sim for sim in self._sims if sim._report._sim is not None] # Filter out None values
936
+
937
+ if show:
938
+ for sim in self._sims:
939
+ sim.Show()
940
+
941
+ class SimpleSimGPU_Report_Compare():
942
+ """ Compare Multiple Simple Simulations GPU """
943
+
944
+ _sims: list[SimpleSimulation]
945
+
946
+ def __init__(self, sims:list[SimpleSimulation | Path | str] | Path, **kwargs):
947
+
948
+ self._sims = []
949
+ self._infos = []
950
+
951
+ if isinstance(sims, Path):
952
+ # We assume it is a a directory containing SimpleSimulation files
953
+
954
+ if sims.is_dir():
955
+ # Load all SimpleSimulation files in the directory
956
+ for sim_file in sims.rglob('parameters.json'):
957
+ try:
958
+ self._sims.append(SimpleSimulation.load(sim_file.parent))
959
+ except Exception as e:
960
+ logging.error(f"Failed to load simulation from file {sim_file.parent}: {e}")
961
+ self._infos.append(f"Failed to load simulation from file {sim_file.parent}: {e}")
962
+ else:
963
+ logging.error(f"The path {sims} is not a directory.")
964
+ self._infos.append(f"The path {sims} is not a directory.")
965
+ return
966
+
967
+ elif isinstance(sims, list):
968
+ for sim in sims:
969
+ if isinstance(sim, Path):
970
+ try:
971
+ self._sims.append(SimpleSimulation.load(sim))
972
+ except Exception as e:
973
+ logging.error(f"Failed to load simulation from path {sim}: {e}")
974
+ self._infos.append(f"Failed to load simulation from path {sim}: {e}")
975
+ elif isinstance(sim, str):
976
+ try:
977
+ self._sims.append(SimpleSimulation.load(Path(sim)))
978
+ except Exception as e:
979
+ logging.error(f"Failed to load simulation from string {sim}: {e}")
980
+ self._infos.append(f"Failed to load simulation from string {sim}: {e}")
981
+ elif not isinstance(sim, SimpleSimulation):
982
+ try:
983
+ self._sims.append(sim)
984
+ except Exception as e:
985
+ logging.error(f"Failed to append simulation {sim}: {e}")
986
+ self._infos.append(f"Failed to append simulation {sim}: {e}")
987
+ else:
988
+ logging.error("Invalid type for simulation. Must be SimpleSimulation, Path, or str.")
989
+ self._infos.append("Invalid type for simulation. Must be SimpleSimulation, Path, or str.")
990
+ return
991
+ else:
992
+ logging.error("Invalid type for simulations. Must be Path, list of SimpleSimulation, Path or str.")
993
+ self._infos.append("Invalid type for simulations. Must be Path, list of SimpleSimulation, Path or str.")
994
+ return
995
+
996
+ self._report = None
997
+
998
+ def _summary_versions(self):
999
+ """ Find the versions of the simulation, wolfhece and the wolfgpu package """
1000
+
1001
+ group_title = "Versions"
1002
+ text = [f"Wolfhece : {wolfhece_version}",
1003
+ f"Wolfgpu : {wolfgpu_version}",
1004
+ f"Python : {sys.version.split()[0]}",
1005
+ f"Operating System: {os.name}"
1006
+ ]
1007
+
1008
+ return group_title, text
1009
+
1010
+ def _summary_array_shapes(self):
1011
+ """ Return the summary of the array shapes of the simulations """
1012
+ if not self._sims:
1013
+ logging.error("No simulations available to summarize.")
1014
+ return
1015
+
1016
+ group_title = "Array Shapes"
1017
+ text = []
1018
+
1019
+ sim_ref = self._sims[0]
1020
+
1021
+ for idx, sim in enumerate(self._sims[1:]):
1022
+ try:
1023
+ text.append(f"{idx} - _bathymetry_: {sim_ref.bathymetry.shape == sim.bathymetry.shape}")
1024
+ except AttributeError:
1025
+ text.append(f"{idx} - _bathymetry_: error")
1026
+ try:
1027
+ text.append(f"{idx} - _manning_: {sim_ref.manning.shape == sim.manning.shape}")
1028
+ except AttributeError:
1029
+ text.append(f"{idx} - _manning_: error")
1030
+ try:
1031
+ text.append(f"{idx} - _infiltration_: {sim_ref.infiltration_zones.shape == sim.infiltration_zones.shape}")
1032
+ except AttributeError:
1033
+ text.append(f"{idx} - _infiltration_: error")
1034
+ try:
1035
+ text.append(f"{idx} - _h_: {sim_ref.h.shape == sim.h.shape}")
1036
+ except AttributeError:
1037
+ text.append(f"{idx} - _h_: error")
1038
+ try:
1039
+ text.append(f"{idx} - _qx_: {sim_ref.qx.shape == sim.qx.shape}")
1040
+ except AttributeError:
1041
+ text.append(f"{idx} - _qx_: error")
1042
+ try:
1043
+ text.append(f"{idx} - _qy_: {sim_ref.qy.shape == sim.qy.shape}")
1044
+ except AttributeError:
1045
+ text.append(f"{idx} - _qy_: error")
1046
+ try:
1047
+ text.append(f"{idx} - _nap_: {sim_ref.nap.shape == sim.nap.shape}")
1048
+ except AttributeError:
1049
+ text.append(f"{idx} - _nap_: error")
1050
+
1051
+ if sim_ref.bridge_roof is not None and sim.bridge_roof is not None:
1052
+ try:
1053
+ text.append(f"{idx} - _bridge roof_: {sim_ref.bridge_roof.shape == sim.bridge_roof.shape}")
1054
+ except AttributeError:
1055
+ text.append(f"{idx} - _bridge roof_: error")
1056
+
1057
+ return group_title, text
1058
+
1059
+ def _summary_array_data(self):
1060
+ """ Return the summary of the array data of the simulations """
1061
+ if not self._sims:
1062
+ logging.error("No simulations available to summarize.")
1063
+ return
1064
+
1065
+ group_title = "Array Data"
1066
+ text = []
1067
+
1068
+ sim_ref = self._sims[0]
1069
+
1070
+ for idx, sim in enumerate(self._sims[1:]):
1071
+ try:
1072
+ text.append(f"{idx} - _bathymetry_: {np.all(sim.bathymetry == sim.bathymetry)}")
1073
+ except AttributeError:
1074
+ text.append(f"{idx} - _bathymetry_: error")
1075
+ try:
1076
+ text.append(f"{idx} - _manning_: {np.all(sim_ref.manning == sim.manning)}")
1077
+ except AttributeError:
1078
+ text.append(f"{idx} - _manning_: error")
1079
+ try:
1080
+ text.append(f"{idx} - _infiltration_: {np.all(sim_ref.infiltration_zones == sim.infiltration_zones)}")
1081
+ except AttributeError:
1082
+ text.append(f"{idx} - _infiltration_: error")
1083
+ try:
1084
+ text.append(f"{idx} - _h_: {np.all(sim_ref.h == sim.h)}")
1085
+ except AttributeError:
1086
+ text.append(f"{idx} - _h_: error")
1087
+ try:
1088
+ text.append(f"{idx} - _qx_: {np.all(sim_ref.qx == sim.qx)}")
1089
+ except AttributeError:
1090
+ text.append(f"{idx} - _qx_: error")
1091
+ try:
1092
+ text.append(f"{idx} - _qy_: {np.all(sim_ref.qy == sim.qy)}")
1093
+ except AttributeError:
1094
+ text.append(f"{idx} - _qy_: error")
1095
+ try:
1096
+ text.append(f"{idx} - _nap_: {np.all(sim_ref.nap == sim.nap)}")
1097
+ except AttributeError:
1098
+ text.append(f"{idx} - _nap_: error")
1099
+ if sim_ref.bridge_roof is not None and sim.bridge_roof is not None:
1100
+ try:
1101
+ text.append(f"{idx} - _bridge roof_: {np.all(sim_ref.bridge_roof == sim.bridge_roof)}")
1102
+ except AttributeError:
1103
+ text.append(f"{idx} - _bridge roof_: error")
1104
+
1105
+ return group_title, text
1106
+
1107
+ def _summary_resolution(self):
1108
+ """ Return the summary of the resolution of the simulations """
1109
+ if not self._sims:
1110
+ logging.error("No simulations available to summarize.")
1111
+ return
1112
+
1113
+ group_title = "Resolution"
1114
+ text = []
1115
+
1116
+ sim_ref = self._sims[0]
1117
+
1118
+ for idx, sim in enumerate(self._sims[1:]):
1119
+ try:
1120
+ text.append(f"{idx} - _base coord ll x_: {sim_ref.param_base_coord_ll_x == sim.param_base_coord_ll_x}")
1121
+ except AttributeError:
1122
+ text.append(f"{idx} - _base coord ll x_: error")
1123
+ try:
1124
+ text.append(f"{idx} - _base coord ll y_: {sim_ref.param_base_coord_ll_y == sim.param_base_coord_ll_y}")
1125
+ except AttributeError:
1126
+ text.append(f"{idx} - _base coord ll y_: error")
1127
+ try:
1128
+ text.append(f"{idx} - _dx_: {sim_ref.param_dx == sim.param_dx}")
1129
+ except AttributeError:
1130
+ text.append(f"{idx} - _dx_: error")
1131
+ try:
1132
+ text.append(f"{idx} - _dy_: {sim_ref.param_dy == sim.param_dy}")
1133
+ except AttributeError:
1134
+ text.append(f"{idx} - _dy_: error")
1135
+ try:
1136
+ text.append(f"{idx} - _nbx_: {sim_ref.param_nx == sim.param_nx}")
1137
+ except AttributeError:
1138
+ text.append(f"{idx} - _nbx_: error")
1139
+ try:
1140
+ text.append(f"{idx} - _nby_: {sim_ref.param_ny == sim.param_ny}")
1141
+ except AttributeError:
1142
+ text.append(f"{idx} - _nby_: error")
1143
+
1144
+ return group_title, text
1145
+
1146
+ def _summary_boundary_conditions(self):
1147
+ """ Return the summary of the boundary conditions of the simulations """
1148
+ if not self._sims:
1149
+ logging.error("No simulations available to summarize.")
1150
+ return
1151
+
1152
+ group_title = "Boundary Conditions"
1153
+ text = []
1154
+
1155
+ sim_ref = self._sims[0]
1156
+
1157
+ for idx, sim in enumerate(self._sims[1:]):
1158
+ try:
1159
+ text.append(f"{idx} - _boundary conditions count_: {len(sim_ref.boundary_condition) == len(sim.boundary_condition)}")
1160
+ except AttributeError:
1161
+ text.append(f"{idx} - _boundary conditions count_: error")
1162
+ bc_set_ref = {bc.ntype: 0 for bc in sim_ref.boundary_condition}
1163
+ for bc in sim_ref.boundary_condition:
1164
+ bc_set_ref[bc.ntype] += 1
1165
+
1166
+ bc_set = {bc.ntype: 0 for bc in sim.boundary_condition}
1167
+ for bc in sim.boundary_condition:
1168
+ bc_set[bc.ntype] += 1
1169
+
1170
+ for bc_type, count in bc_set_ref.items():
1171
+ if count > 0:
1172
+ try:
1173
+ text.append(f"{idx} - _{count} {bc_type.name}_: {bc_set.get(bc_type, 0) == count}")
1174
+ except AttributeError:
1175
+ text.append(f"{idx} - _{count} {bc_type.name}_: error")
1176
+
1177
+ return group_title, text
1178
+
1179
+ def summary(self):
1180
+ """ Create a summary of the simulations """
1181
+ summary = {}
1182
+ summary['array_shapes'] = self._summary_array_shapes()
1183
+ summary['array_data'] = self._summary_array_data()
1184
+ summary['resolution'] = self._summary_resolution()
1185
+ summary['boundary_conditions'] = self._summary_boundary_conditions()
1186
+
1187
+ return summary
1188
+
1189
+ def _html_table_compare(self, key:str, text: list):
1190
+ """ Create an HTML table to compare the simulations
1191
+
1192
+ One line for each parameter to compare.
1193
+ One column for each simulation.
1194
+ The first column is the parameter name.
1195
+ The first row is the simulation name.
1196
+ """
1197
+
1198
+ html = "<table style='border-collapse: collapse; width: 100%;'>"
1199
+ html += "<tr><th>Parameter</th>"
1200
+ for sim in self._sims[1:]:
1201
+ html += f"<th>{sim.path.name}</th>"
1202
+ html += "</tr>"
1203
+
1204
+ params = [current_text.split(': ', 1)[0].split(' - ')[-1] for current_text in text if '0 -' in current_text]
1205
+
1206
+ for param in params:
1207
+ html += f"<tr><td>{param[1:-1]}</td>"
1208
+
1209
+ # find the element in the text that contains the parameter name
1210
+ for current_text in text:
1211
+ if param in current_text:
1212
+ value = current_text.split(': ', 1)[-1]
1213
+ if value == 'True':
1214
+ html += f'<td><span style="color: green; font-size: 14px;">\u2705</span></td>'
1215
+ else:
1216
+ html += f'<td><span style="color: red; font-size: 14px;">\u274C</span></td>'
1217
+ html += "</tr>"
1218
+
1219
+ html += "</table>"
1220
+
1221
+ # add basic css like border, padding, and font size, grid
1222
+ html = f"<div style='font-size: 8pt; font-family: Helvetica; border: 1px solid #ddd; padding: 5px; text-align: center'>{html}</div>"
1223
+
1224
+ return html
1225
+
1226
+ def _layout(self):
1227
+ """ Set the layout of the PDF report for the comparison."""
1228
+
1229
+ summary = self.summary()
1230
+
1231
+ LEFT_MARGIN = 1
1232
+ TOP_MARGIN = 0.5
1233
+ PADDING = 0.5
1234
+
1235
+ WIDTH_TITLE = 16
1236
+ HEIGHT_TITLE = 1.5
1237
+ WIDTH_VERSIONS = 16
1238
+ HEIGHT_VERSIONS = 0.5
1239
+ X_LOGO = 18.5
1240
+ WIDTH_LOGO = 1.5
1241
+ HEIGHT_LOGO = 1.5
1242
+
1243
+ WIDTH_SUMMARY = 19
1244
+ HEIGHT_SUMMARY = 5
1245
+
1246
+ WIDTH_REFERENCE = 19
1247
+ HEIGHT_REFERENCE = 1.5
1248
+
1249
+ layout = {}
1250
+ layout['title'] = rect_cm(LEFT_MARGIN, TOP_MARGIN, WIDTH_TITLE, HEIGHT_TITLE)
1251
+ layout['versions'] = rect_cm(LEFT_MARGIN, TOP_MARGIN + HEIGHT_TITLE + PADDING, WIDTH_VERSIONS, HEIGHT_VERSIONS)
1252
+ layout['reference'] = rect_cm(LEFT_MARGIN, TOP_MARGIN + HEIGHT_TITLE + HEIGHT_VERSIONS + 1.5*PADDING, WIDTH_REFERENCE, HEIGHT_REFERENCE)
1253
+ layout['logo'] = rect_cm(X_LOGO, TOP_MARGIN, WIDTH_LOGO, HEIGHT_LOGO)
1254
+ layout['footer'] = rect_cm(LEFT_MARGIN, 28, 19, 1.2) # Footer at the bottom
1255
+
1256
+ TOP_SUMMARY = TOP_MARGIN + HEIGHT_TITLE + HEIGHT_VERSIONS + HEIGHT_REFERENCE + 2 * PADDING # 1.5 cm for title, 1 cm for versions, 0.5 cm padding
1257
+ y_summary = TOP_SUMMARY
1258
+
1259
+ layout['resolution'] = rect_cm(LEFT_MARGIN, y_summary, WIDTH_SUMMARY, HEIGHT_SUMMARY *2/3)
1260
+ y_summary += HEIGHT_SUMMARY*2/3 + 2*PADDING
1261
+
1262
+ layout['array_shapes'] = rect_cm(LEFT_MARGIN, y_summary, WIDTH_SUMMARY, HEIGHT_SUMMARY)
1263
+ y_summary += HEIGHT_SUMMARY + 2*PADDING
1264
+
1265
+ layout['array_data'] = rect_cm(LEFT_MARGIN, y_summary, WIDTH_SUMMARY, HEIGHT_SUMMARY)
1266
+ y_summary += HEIGHT_SUMMARY + 2*PADDING
1267
+
1268
+ layout['boundary_conditions'] = rect_cm(LEFT_MARGIN, y_summary, WIDTH_SUMMARY, HEIGHT_SUMMARY/2)
1269
+ y_summary += HEIGHT_SUMMARY/2 + 2*PADDING
1270
+
1271
+ layout['informations'] = rect_cm(LEFT_MARGIN, y_summary, WIDTH_SUMMARY, HEIGHT_SUMMARY)
1272
+ y_summary += HEIGHT_SUMMARY + 2*PADDING
1273
+
1274
+ return layout, summary
1275
+
1276
+ def create_report(self):
1277
+ """ Create the PDF report for the comparison """
1278
+ if not self._sims:
1279
+ logging.error("No simulation data available to create report.")
1280
+ return
1281
+
1282
+ # Create a new PDF document
1283
+ self._doc = pdf.Document()
1284
+
1285
+ # Add a page
1286
+ page = self._doc.new_page()
1287
+
1288
+ layout, summary = self._layout()
1289
+
1290
+ page.insert_htmlbox(layout['title'], f"<h1>GPU - Parameters comparison report</h1>",
1291
+ css='h1 {font-size:16pt; font-family:Helvetica; color:#333}')
1292
+
1293
+ # versions
1294
+ group, summary_versions = self._summary_versions()
1295
+ rect = layout['versions']
1296
+ html, css = list_to_html_aligned(summary_versions, font_size="6pt", font_family="Helvetica")
1297
+ page.insert_htmlbox(rect, html, css=css, scale_low = 0.1)
1298
+
1299
+ # reference
1300
+ rect = layout['reference']
1301
+ if self._sims:
1302
+ ref_sim = self._sims[0]
1303
+ ref_text = [f"Reference Simulation: {ref_sim.path.name}",
1304
+ f"Resolution (dx, dy): ({ref_sim.param_dx}, {ref_sim.param_dy})",
1305
+ f"Number of Cells (nx, ny): ({ref_sim.param_nx}, {ref_sim.param_ny})",
1306
+ # full path
1307
+ f"Full Path: {ref_sim.path.resolve()}"]
1308
+ html, css = list_to_html_aligned(ref_text, font_size="8pt", font_family="Helvetica")
1309
+ page.insert_htmlbox(rect, html, css=css, scale_low = 0.1)
1310
+
1311
+ for key, text in summary.items():
1312
+ rect = layout[key]
1313
+
1314
+ # Add a rectangle for the group
1315
+ page.draw_rect(rect, color=(0, 0, 0, .05), width=0.5)
1316
+
1317
+ # Add the group title
1318
+ page.insert_text((rect.x0 + 1, rect.y0 - cm2pts(.2)), text[0],
1319
+ fontsize=12, fontname="helv", fill=(0, 0, 0), fill_opacity=1.)
1320
+
1321
+ # Create an HTML table for the comparison
1322
+ html = self._html_table_compare(text[0], text[1])
1323
+ spare_height, scale = page.insert_htmlbox(rect, html,
1324
+ scale_low=0.1)
1325
+
1326
+ if spare_height < 0.:
1327
+ logging.warning("Text overflow in summary box. Adjusting scale.")
1328
+
1329
+ # logo
1330
+ rect = layout['logo']
1331
+ # Add the logo to the top-right corner
1332
+ logo_path = Path(__file__).parent.parent / 'apps' / 'WolfPython2.png'
1333
+ if logo_path.exists():
1334
+ page.insert_image(rect, filename=str(logo_path), keep_proportion=True,
1335
+ overlay=True)
1336
+ # Footer
1337
+ # ------
1338
+ # Insert the date and time of the report generation, the user and the PC name
1339
+ footer_rect = layout['footer']
1340
+ footer_text = f"<p>Report generated on {dt.now()} by {os.getlogin()} on {platform.uname().node} - {platform.uname().machine} - {platform.uname().release} - {platform.uname().version}</br> \
1341
+ This report does not guarantee the quality of the simulation and in no way commits the software developers.</p>"
1342
+ page.insert_htmlbox(footer_rect, footer_text,
1343
+ css='p {font-size:10pt; font-family:Helvetica; color:#BEBEBE; align-text:center}',)
1344
+
1345
+ # Infos
1346
+ # -----
1347
+
1348
+ rect = layout['informations']
1349
+ if self._infos:
1350
+ page.insert_text((rect.x0 + 1, rect.y0 + cm2pts(.05)), "Informations / Warnings / Errors",
1351
+ fontsize=10, fontname="helv", fill=(0, 0, 0), fill_opacity=1.)
1352
+ html, css = list_to_html(self._infos, font_size="8pt", font_family="Helvetica")
1353
+ spare_height, scale = page.insert_htmlbox(rect, html, css=css,
1354
+ scale_low=0.1)
1355
+ if spare_height < 0.:
1356
+ logging.warning("Text overflow in informations box. Adjusting scale.")
1357
+
1358
+
1359
+ def save_report(self, output_path: Path | str = None):
1360
+ """ Save the report to a PDF file """
1361
+
1362
+ # Save the PDF to a file
1363
+ if output_path is None:
1364
+ output_path = Path("GPU_Comparison_Report.pdf")
1365
+
1366
+ try:
1367
+ self._doc.subset_fonts()
1368
+ self._doc.save(output_path, garbage=3, deflate=True)
1369
+ self._pdf_path = output_path
1370
+ except Exception as e:
1371
+ logging.error(f"Failed to save the report to {output_path}: {e}")
1372
+ logging.error("Check if the file is already opened.")
1373
+ self._pdf_path = None
1374
+ return
1375
+
1376
+ @property
1377
+ def pdf_path(self):
1378
+ """ Return the PDF document """
1379
+ return self._pdf_path
1380
+
1381
+ class SimpleSimGPU_Report_Compare_wx(PDFViewer):
1382
+
1383
+ def __init__(self, sims:list[SimpleSimulation | Path | str], **kwargs):
1384
+ """ Initialize the Simple Simulation GPU Report Viewer for comparison """
1385
+
1386
+ super(SimpleSimGPU_Report_Compare_wx, self).__init__(None, **kwargs)
1387
+
1388
+ self._report = SimpleSimGPU_Report_Compare(sims, **kwargs)
1389
+
1390
+ self._report.create_report()
1391
+ self._report.save_report()
1392
+
1393
+ # Load the PDF into the viewer
1394
+ if self._report._pdf_path is None:
1395
+ logging.error("No report created. Cannot load PDF.")
1396
+ return
1397
+
1398
+ self.load_pdf(self._report.pdf_path)
1399
+ self.viewer.SetZoom(-1) # Fit to width
1400
+
1401
+ # Set the title of the frame
1402
+ self.SetTitle("Simple Simulation GPU Comparison Report")
1403
+
1404
+ self.Bind(wx.EVT_CLOSE, self.on_close)
1405
+
1406
+ def on_close(self, event):
1407
+ """ Handle the close event to clean up resources """
1408
+ self.viewer.pdfdoc.pdfdoc.close()
1409
+ self.Destroy()