wolfhece 2.2.27__py3-none-any.whl → 2.2.29__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.
@@ -11,6 +11,7 @@ from tempfile import NamedTemporaryFile
11
11
  from datetime import datetime as dt
12
12
 
13
13
  import matplotlib.pyplot as plt
14
+ import matplotlib as mpl
14
15
  import seaborn as sns
15
16
 
16
17
  import pymupdf as pdf
@@ -19,111 +20,18 @@ from wolfgpu.simple_simulation import InfiltrationChronology, SimulationDuration
19
20
  from wolfgpu.simple_simulation import boundary_condition_2D, BoundaryConditionsTypes
20
21
 
21
22
  from .pdf import PDFViewer
23
+ from .common import cm2pts, A4_rect, rect_cm, cm2inches, list_to_html, list_to_html_aligned, get_rect_from_text
22
24
  from ..wolf_array import WolfArray, header_wolf
23
25
  from ..PyTranslate import _
24
26
  from .. import __version__ as wolfhece_version
25
27
  from wolfgpu.version import __version__ as wolfgpu_version
26
28
 
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
29
  class SimpleSimGPU_Report():
123
30
 
124
31
  def __init__(self, sim:SimpleSimulation | Path | str, **kwargs):
125
32
  """ Initialize the Simple Simulation GPU Report Viewer """
126
33
 
34
+ self._summary = {}
127
35
  self._doc = None
128
36
 
129
37
  if isinstance(sim, Path):
@@ -132,6 +40,7 @@ class SimpleSimGPU_Report():
132
40
  except Exception as e:
133
41
  logging.error(f"Failed to load simulation from path {sim}: {e}")
134
42
  self._sim = None
43
+ self._summary['errors'] = e
135
44
  return
136
45
  elif isinstance(sim, str):
137
46
  try:
@@ -139,6 +48,7 @@ class SimpleSimGPU_Report():
139
48
  except Exception as e:
140
49
  logging.error(f"Failed to load simulation from string path {sim}: {e}")
141
50
  self._sim = None
51
+ self._summary['errors'] = e
142
52
  return
143
53
  elif not isinstance(sim, SimpleSimulation):
144
54
  try:
@@ -146,12 +56,12 @@ class SimpleSimGPU_Report():
146
56
  except Exception as e:
147
57
  logging.error(f"Failed to set simulation: {e}")
148
58
  self._sim = None
59
+ self._summary['errors'] = e
149
60
  return
150
61
  else:
151
62
  logging.error("Invalid type for simulation. Must be SimpleSimulation, Path, or str.")
152
63
  return
153
64
 
154
- self._summary = {}
155
65
  self._summary['warnings'] = self._summary_warnings()
156
66
  self._summary['errors'] = self._summary_errors()
157
67
 
@@ -366,9 +276,13 @@ class SimpleSimGPU_Report():
366
276
  # Plot the histogram of waterdepth adn add it to the PDF
367
277
  fig, ax = plt.subplots(figsize=(8, 6))
368
278
  # 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
279
+
280
+ h_min = np.min(sim.h[sim.nap == 1])
281
+ h_max = np.max(sim.h[sim.nap == 1])
282
+
283
+ if h_max > h_min:
284
+ ax.hist(sim.h[sim.h > 0.], bins=100, density=True)
285
+ ax.set_xlim(0, h_max) # Set xlim to 110% of max value
372
286
  ax.set_xlabel('Water Depth [m]')
373
287
  ax.set_ylabel('Frequency')
374
288
 
@@ -395,7 +309,7 @@ class SimpleSimGPU_Report():
395
309
  ax.hist(sim.manning[sim.nap == 1], bins=100, density = True)
396
310
  # ax.set_title('Histogram of Manning Coefficient')
397
311
  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
312
+ ax.set_xlim(0, np.max(sim.manning[sim.nap == 1]) * 1.1) # Set xlim to 110% of max value
399
313
  ax.set_ylabel('Frequency')
400
314
 
401
315
  # set font size of the labels
@@ -865,15 +779,21 @@ class SimpleSimGPU_Report():
865
779
 
866
780
  class SimpleSimGPU_Report_wx(PDFViewer):
867
781
 
868
- def __init__(self, sim:SimpleSimulation | Path | str, **kwargs):
782
+ def __init__(self, sim:SimpleSimulation | Path | str, show:bool=False, **kwargs):
869
783
  """ Initialize the Simple Simulation GPU Report Viewer """
870
784
 
785
+ mpl.use('Agg') # Use a non-interactive backend for matplotlib
786
+
871
787
  super(SimpleSimGPU_Report_wx, self).__init__(None, **kwargs)
872
788
 
873
789
  self._report = SimpleSimGPU_Report(sim, **kwargs)
874
790
 
875
791
  if self._report._sim is None:
876
792
  logging.error("No simulation data available to create report.")
793
+ dlg = wx.MessageDialog(self, "No simulation data available to create report.\n\nPlease check the errors in the logs.",
794
+ "Error", wx.OK | wx.ICON_ERROR)
795
+ dlg.ShowModal()
796
+ dlg.Destroy()
877
797
  return
878
798
 
879
799
  self._report.create_report()
@@ -892,6 +812,11 @@ class SimpleSimGPU_Report_wx(PDFViewer):
892
812
 
893
813
  self.Bind(wx.EVT_CLOSE, self.on_close)
894
814
 
815
+ if show:
816
+ self.Show()
817
+
818
+ mpl.use('WxAgg') # Reset matplotlib to use the WxAgg backend for other plots
819
+
895
820
  def on_close(self, event):
896
821
  """ Handle the close event of the frame """
897
822
 
wolfhece/report/tools.py CHANGED
@@ -376,34 +376,57 @@ class Analysis_Scenarios():
376
376
 
377
377
  def __init__(self, base_directory: Path | str, storage_directory: Path | str = None, name:str = ''):
378
378
 
379
+ # Default figure size for plots
380
+ self._fig_size = (20, 10)
381
+
382
+ # Name of the analysis scenarios instance
379
383
  self.name = name.strip() if name else 'Analysis_Scenarios'
384
+
385
+ # Base directory for the analysis scenarios
386
+ # Must contains the directories 'projets', 'rapports', 'vecteurs', 'nuages_de_points', 'images' and 'cache'
380
387
  self.base_directory = Path(base_directory)
388
+
389
+ # Storage directory for the analysis scenarios - Default is the base directory
381
390
  self.storage_directory = storage_directory if storage_directory is not None else self.base_directory
382
391
 
392
+ # Check if the base directory exists and contains the necessary directories
383
393
  self.check_directories()
394
+ # Fill the directories attribute with the paths of the analysis directories
384
395
  self.directories = get_directories_as_dict(self.base_directory)
385
396
 
397
+ # Initialize the scenarios directories
386
398
  self.scenarios_directories:dict[str:Path]
387
399
  self.scenarios_directories = {}
400
+ # List of scenario names
388
401
  self.scenarios = []
389
402
 
390
403
  self.current_scenario = None
391
- self.report = None
404
+ self._return_periods = []
405
+
406
+ # RapidReport associated with the analysis scenarios
407
+ self.report:RapidReport = None
392
408
  self._report_name = 'analysis_report.docx'
393
409
  self._report_saved_once = False
394
- self.mapviewer = None
395
- self._background_images = None
396
410
 
397
- self._polygons = {}
411
+ # Map viewer for the analysis scenarios
412
+ self.mapviewer:WolfMapViewer = None
398
413
 
399
- self._return_periods = []
414
+ # Name of the WMS service
415
+ self._background_images:str = None
416
+
417
+ # Polygons associated with the analysis scenarios
418
+ self._polygons:dict[str, Polygons_Analyze] = {}
400
419
 
420
+ self._reference_polygon:Polygons_Analyze = None
421
+
422
+ # Modifications associated with the analysis scenarios
401
423
  self._modifications = {}
402
424
 
425
+ # MultiProjects associated with the analysis scenarios.
426
+ # One project contains multiple suimulations.
403
427
  self._multiprojects = None
404
- self._cached_date = False
405
428
 
406
- self._reference_polygon:Polygons_Analyze = None
429
+ # Landmarks and measures associated with the analysis
407
430
  self._landmarks:Zones = None
408
431
  self._landmarks_s_label = []
409
432
 
@@ -414,10 +437,32 @@ class Analysis_Scenarios():
414
437
  self._cloud:list[tuple[float, float, str]] = [] # List of tuples (s, z, label) for point clouds
415
438
 
416
439
  self._images = {}
417
- self._zoom = {}
440
+
441
+ # Zoom (smin, smax, zmin, zmax) for the analysis
442
+ self._zoom:dict[str, tuple[float,float,float,float]] = {}
418
443
 
419
444
  logging.info(f"Analysis directories initialized: {self.directories}")
420
445
 
446
+ @property
447
+ def fig_size(self) -> tuple[float]:
448
+ """ Return the default figure size for plots.
449
+
450
+ :return: A tuple (width, height) representing the default figure size.
451
+ """
452
+ return self._fig_size
453
+
454
+ @fig_size.setter
455
+ def fig_size(self, size:tuple[float]) -> None:
456
+ """ Set the default figure size for plots.
457
+
458
+ :param size: A tuple (width, height) representing the default figure size.
459
+ """
460
+ if not isinstance(size, tuple) or len(size) != 2:
461
+ logging.error("Default figure size must be a tuple of (width, height).")
462
+ raise ValueError("Default figure size must be a tuple of (width, height).")
463
+ self._fig_size = size
464
+ logging.info(f"Default figure size set to {self._fig_size}.")
465
+
421
466
  def add_zoom(self, label:str, bounds:tuple[float]) -> None:
422
467
  """ Add a zoom level to the analysis.
423
468
 
@@ -669,7 +714,13 @@ class Analysis_Scenarios():
669
714
  logging.info(f"Landmark '{label}' added at coordinates ({x}, {y}).")
670
715
 
671
716
  def update_landmark(self, label: str, s_xy:float |tuple[float] = None, z: float = None) -> None:
672
- """ Update a landmark in the analysis. """
717
+ """ Update a landmark in the analysis.
718
+
719
+ :param label: The label of the landmark to update.
720
+ :param s_xy: The s-coordinate or a tuple (s, xy) to update the landmark's position.
721
+ :param z: The z-coordinate to update the landmark's elevation (optional).
722
+ """
723
+
673
724
  if self._landmarks is None:
674
725
  logging.error("No landmarks have been added to the analysis.")
675
726
  raise ValueError("No landmarks have been added to the analysis.")
@@ -701,7 +752,12 @@ class Analysis_Scenarios():
701
752
  self._landmarks_s_label[i] = (s_xy, z, label)
702
753
 
703
754
  def plot_cloud(self, ax: plt.Axes, bounds:tuple[float]) -> plt.Axes:
755
+ """ Trace the cloud of points on an axis Matplotlib
704
756
 
757
+ :param ax: axe Matplotlib
758
+ :param bounds: tuple (xmin, xmax, ymin, ymax) for the plot limits
759
+ :return: The Matplotlib Axes object with the cloud plotted.
760
+ """
705
761
  xmin, xmax, ymin, ymax = bounds
706
762
 
707
763
  used_cloud = [(s, z, label) for s, z, label in self._cloud if s >= xmin and s <= xmax]
@@ -713,7 +769,16 @@ class Analysis_Scenarios():
713
769
  return ax
714
770
 
715
771
  def plot_measures(self, ax:plt.Axes, bounds:tuple[float], style:dict = None) -> plt.Axes:
772
+ """ Trace les mesures sur un axe Matplotlib
773
+
774
+ :param ax: axe Matplotlib
775
+ :param bounds: tuple (xmin, xmax, ymin, ymax) for the plot limits
776
+ :param style: Optional style dictionary for the measures. Available properties:
777
+ - 'color': The color of the line.
778
+ - 'linestyle': The style of the line (e.g., '-', '--', '-.', ':').
716
779
 
780
+ :return: The Matplotlib Axes object with the measures plotted.
781
+ """
717
782
  xmin, xmax, ymin, ymax = bounds
718
783
  i=0
719
784
  for key, measure in self._projected_measures.items():
@@ -777,13 +842,17 @@ class Analysis_Scenarios():
777
842
  bounds:tuple[float] | str,
778
843
  operator:operators = operators.MEDIAN,
779
844
  plot_annex:bool = True,
780
- save:bool = False) -> tuple[plt.Figure, plt.Axes]:
845
+ save:bool = False,
846
+ figsize:tuple[float] = None) -> tuple[plt.Figure, plt.Axes]:
781
847
  """ Plot the waterlines for a specific scenario.
782
848
 
783
849
  :param scenario: The name of the scenario to plot waterlines for or a list of scenarios for comparison.
784
850
  :param bounds: A tuple (xmin, xmax, ymin, ymax) representing the zoom bounds or a string label for a zoom level.
785
851
  :param operator: The operator to apply on the waterlines.
786
852
  :param save: If True, save the plot as an image file.
853
+ :param figsize: A tuple (width, height) representing the size of the figure. If None, uses the default figure size.
854
+ :param plot_annex: If True, plot the cloud of points, measures, and landmarks.
855
+ :return: A tuple (fig, ax) where fig is the matplotlib Figure and ax is the matplotlib Axes object.
787
856
  """
788
857
 
789
858
  if isinstance(bounds, str):
@@ -850,7 +919,14 @@ class Analysis_Scenarios():
850
919
 
851
920
  filename = self.directories[Directory_Analysis.IMAGES] / f"{self.name}_{ref}_{str(xmin)}_{str(xmax)}_waterlines_comparison.png"
852
921
 
853
- fig.set_size_inches(20,10)#15
922
+ if figsize is not None:
923
+ if not isinstance(figsize, tuple) or len(figsize) != 2:
924
+ logging.error("Figure size must be a tuple of (width, height).")
925
+ raise ValueError("Figure size must be a tuple of (width, height).")
926
+ fig.set_size_inches(figsize)
927
+ else:
928
+ fig.set_size_inches(self.fig_size[0], self.fig_size[1]) # Use the default figure size
929
+
854
930
  ax.legend()
855
931
  #zoomA
856
932
  ax.set_xlim(xmin, xmax)
@@ -879,7 +955,8 @@ class Analysis_Scenarios():
879
955
  bounds:tuple[float] | str,
880
956
  operator:operators = operators.MEDIAN,
881
957
  plot_annex:bool = True,
882
- save:bool = False) -> tuple[plt.Figure, plt.Axes]:
958
+ save:bool = False,
959
+ figsize:tuple[float] = None) -> tuple[plt.Figure, plt.Axes]:
883
960
  """ Plot the heads for a specific scenario.
884
961
 
885
962
  :param scenario: The name of the scenario to plot heads for or a list of scenarios for comparison.
@@ -887,6 +964,8 @@ class Analysis_Scenarios():
887
964
  :param operator: The operator to apply on the heads.
888
965
  :param plot_annex: If True, plot the cloud of points, measures, and landmarks.
889
966
  :param save: If True, save the plot as an image file.
967
+ :param figsize: A tuple (width, height) representing the figure size. If None, use the default figure size.
968
+ :return: A tuple (fig, ax) representing the figure and axes of the plot
890
969
  """
891
970
  if isinstance(bounds, str):
892
971
  # If bounds is a string, assume it's a label for a zoom level
@@ -922,7 +1001,14 @@ class Analysis_Scenarios():
922
1001
  ax.plot(s, z, label=f"{scen_name} - {sim_name}", linestyle='--', linewidth=1.5)
923
1002
  filename = self.directories[Directory_Analysis.IMAGES] / f"{self.name}_{ref}_{str(xmin)}_{str(xmax)}_heads_comparison.png"
924
1003
 
925
- fig.set_size_inches(20, 10)
1004
+ if figsize is not None:
1005
+ if not isinstance(figsize, tuple) or len(figsize) != 2:
1006
+ logging.error("Figure size must be a tuple of (width, height).")
1007
+ raise ValueError("Figure size must be a tuple of (width, height).")
1008
+ fig.set_size_inches(figsize)
1009
+ else:
1010
+ fig.set_size_inches(self.fig_size[0], self.fig_size[1]) # Use the default figure size
1011
+
926
1012
  ax.legend()
927
1013
  #zoomA
928
1014
  ax.set_xlim(xmin, xmax)
@@ -948,13 +1034,17 @@ class Analysis_Scenarios():
948
1034
  bounds:tuple[float] | str,
949
1035
  operator:operators = operators.MEDIAN,
950
1036
  plot_annex:bool = True,
951
- save:bool = False) -> tuple[plt.Figure, plt.Axes]:
1037
+ save:bool = False,
1038
+ figsize:tuple[float] = None) -> tuple[plt.Figure, plt.Axes]:
952
1039
  """ Plot the Froude for a specific scenario.
953
1040
 
954
1041
  :param scenario: The name of the scenario to plot waterlines for or a list of scenarios for comparison.
955
1042
  :param bounds: A tuple (xmin, xmax, ymin, ymax) representing the zoom bounds or a string label for a zoom level.
956
1043
  :param operator: The operator to apply on the waterlines.
957
1044
  :param save: If True, save the plot as an image file.
1045
+ :param figsize: A tuple (width, height) representing the figure size. If None, use the default figure size.
1046
+ :param plot_annex: If True, plot the cloud of points, measures, and landmarks.
1047
+ :return: A tuple (fig, ax) representing the figure and axes of the plot
958
1048
  """
959
1049
 
960
1050
  if isinstance(bounds, str):
@@ -1015,7 +1105,14 @@ class Analysis_Scenarios():
1015
1105
 
1016
1106
  filename = self.directories[Directory_Analysis.IMAGES] / f"{self.name}_{ref}_{str(xmin)}_{str(xmax)}_Froude_comparison.png"
1017
1107
 
1018
- fig.set_size_inches(20,10)#15
1108
+ if figsize is not None:
1109
+ if not isinstance(figsize, tuple) or len(figsize) != 2:
1110
+ logging.error("Figure size must be a tuple of (width, height).")
1111
+ raise ValueError("Figure size must be a tuple of (width, height).")
1112
+ fig.set_size_inches(figsize)
1113
+ else:
1114
+ fig.set_size_inches(self.fig_size[0], self.fig_size[1]) # Use the default figure size
1115
+
1019
1116
  ax.legend()
1020
1117
  #zoomA
1021
1118
  ax.set_xlim(xmin, xmax)
@@ -1739,6 +1836,8 @@ class Analysis_Scenarios():
1739
1836
 
1740
1837
  :param scenario_name: The name of the scenario to set as current.
1741
1838
  """
1839
+
1840
+ scenario_name = _sanitize_scenario_name(scenario_name)
1742
1841
  if scenario_name not in self.scenarios_directories:
1743
1842
  logging.error(f"Scenario '{scenario_name}' not found in the analysis.")
1744
1843
  raise ValueError(f"Scenario '{scenario_name}' not found in the analysis.")
@@ -1752,6 +1851,8 @@ class Analysis_Scenarios():
1752
1851
  :param scenario_name: The name of the scenario to get.
1753
1852
  :return: The path to the scenario directory.
1754
1853
  """
1854
+
1855
+ scenario_name = _sanitize_scenario_name(scenario_name)
1755
1856
  if scenario_name not in self.scenarios_directories:
1756
1857
  logging.error(f"Scenario '{scenario_name}' not found in the analysis.")
1757
1858
  raise KeyError(f"Scenario '{scenario_name}' not found in the analysis.")
@@ -1863,7 +1964,11 @@ L'attention est toutefois attirée sur le fait que cette approche pourrait ne pa
1863
1964
  return fig, ax
1864
1965
 
1865
1966
  def load_modifications(self, ad2viewer:bool = True):
1866
- """ Load modifications for scenarios from vecz files."""
1967
+ """ Load modifications for scenarios from vecz files.
1968
+
1969
+ :param ad2viewer: If True, add the modifications to the map viewer.
1970
+ :raises ValueError: If the MapViewer is not initialized.
1971
+ """
1867
1972
 
1868
1973
  MODIF = 'bath_assembly.vecz'
1869
1974