wolfhece 2.2.45__py3-none-any.whl → 2.2.46__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.
wolfhece/report/common.py CHANGED
@@ -3,9 +3,12 @@ import wx
3
3
  import os
4
4
  import platform
5
5
 
6
+ import pandas as pd
6
7
  import pymupdf as pdf
7
8
  from tempfile import NamedTemporaryFile
8
9
  from tempfile import TemporaryDirectory
10
+ from functools import cached_property
11
+ from PIL import Image
9
12
 
10
13
  import matplotlib.pyplot as plt
11
14
  import numpy as np
@@ -159,6 +162,81 @@ li {{
159
162
 
160
163
  return html, css
161
164
 
165
+
166
+ def dict_to_html(data_dict:dict, font_size="10pt", font_family="Helvetica"):
167
+ """ Convert a dictionary to an HTML table with dataframe_image """
168
+
169
+ df = pd.DataFrame.from_dict(data_dict)
170
+
171
+ # Convert the DataFrame to an HTML table
172
+ html = df.to_html(index=False, border=0, justify='left')
173
+
174
+ # Generate the CSS
175
+ css = f"""
176
+ table {{
177
+ font-size: {font_size};
178
+ font-family: {font_family};
179
+ color: #2C3E50;
180
+ border-collapse: collapse;
181
+ }}
182
+ th, td {{
183
+ padding: 8px 12px;
184
+ border: 1px solid #BDC3C7;
185
+ }}
186
+ """
187
+
188
+ return html, css
189
+
190
+ def dataframe_to_html(data:pd.DataFrame, font_size="10pt", font_family="Helvetica"):
191
+ """ Convert a DataFrame to an HTML table with dataframe_image """
192
+
193
+ # Convert the DataFrame to an HTML table
194
+ html = data.to_html(index=False, border=0, justify='left')
195
+
196
+ # Generate the CSS
197
+ css = f"""
198
+ table {{
199
+ font-size: {font_size};
200
+ font-family: {font_family};
201
+ color: #2C3E50;
202
+ border-collapse: collapse;
203
+ }}
204
+ th, td {{
205
+ padding: 8px 12px;
206
+ border: 1px solid #BDC3C7;
207
+ }}
208
+ """
209
+
210
+ return html, css
211
+
212
+ def convert_report_to_images(pdf_path: str | Path, dpi=150) -> list[Image.Image]:
213
+ """ Convert the PDF report to a list of images (one per page).
214
+
215
+ :param dpi: Dots per inch for the output images.
216
+ :return: List of Images, one per page.
217
+ """
218
+
219
+ if pdf_path is None:
220
+ raise ValueError("PDF report has not been saved yet. Please save the report before converting to images.")
221
+
222
+ pdf_path = Path(pdf_path)
223
+ if not pdf_path.exists():
224
+ raise FileNotFoundError(f"PDF report not found at {pdf_path}. Please check the path.")
225
+
226
+ doc = pdf.open(pdf_path)
227
+ images = []
228
+
229
+ zoom = dpi / 72 # 72 dpi is the default resolution
230
+ mat = pdf.Matrix(zoom, zoom)
231
+
232
+ for page_num in range(len(doc)):
233
+ page = doc.load_page(page_num)
234
+ pix = page.get_pixmap(matrix=mat, alpha=False)
235
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
236
+ images.append(img)
237
+
238
+ return images
239
+
162
240
  # A4 format
163
241
  PAGE_WIDTH = 21 # cm
164
242
  PAGE_HEIGHT = 29.7 # cm
@@ -169,7 +247,10 @@ SLIDE_WIDTH = inches2cm(13.3333) # cm
169
247
 
170
248
  class DefaultLayoutA4(Zones):
171
249
  """
172
- Enum for default layout options.
250
+ Global layout for A4 report.
251
+
252
+ This class inherits from Zones and defines a standard layout for A4 reports.
253
+ It includes predefined areas for the title, versions, logo, and footer.
173
254
  """
174
255
 
175
256
  def __init__(self, title:str, filename = '', ox = 0, oy = 0, tx = 0, ty = 0, parent=None, is2D=True, idx = '', plotted = True, mapviewer=None, need_for_wx = False, bbox = None, find_minmax = True, shared = False, colors = None):
@@ -260,14 +341,21 @@ class DefaultLayoutA4(Zones):
260
341
  vec_footer.find_minmax()
261
342
  elts.add_vector(vec_footer, forceparent=True)
262
343
 
263
- self._layout = {}
264
344
  self._doc = None # Placeholder for the PDF document
265
345
  self._pdf_path = None # Placeholder for the PDF file path
266
346
 
267
347
  def add_element(self, name:str, width:float, height:float, x:float = 0, y:float = 0) -> vector:
268
348
  """
269
- Add an element to the layout.
349
+ Add a single element to the page.
350
+
351
+ :param name: Name of the element.
352
+ :param width: Width of the element in cm.
353
+ :param height: Height of the element in cm.
354
+ :param x: X coordinate of the bottom-left corner in cm.
355
+ :param y: Y coordinate of the bottom-left corner in cm.
356
+ :return: The created vector representing the element.
270
357
  """
358
+
271
359
  vec = vector(name=name)
272
360
  vec.add_vertex(wv(x, y))
273
361
  vec.add_vertex(wv(x + width, y))
@@ -287,6 +375,19 @@ class DefaultLayoutA4(Zones):
287
375
  first_x:float = 0, first_y:float = 0,
288
376
  count_x:int = 1, count_y:int = 1,
289
377
  padding:float = None) -> zone:
378
+ """
379
+ Add multiple elements to the page in a grid.
380
+
381
+ :param name: Base name for the elements.
382
+ :param width: Width of each element in cm.
383
+ :param height: Height of each element in cm.
384
+ :param first_x: X coordinate of the first element in cm.
385
+ :param first_y: Y coordinate of the first element in cm.
386
+ :param count_x: Number of elements in the X direction (positive for right, negative for left).
387
+ :param count_y: Number of elements in the Y direction (positive for up, negative for down).
388
+ :param padding: Padding between elements in cm. If None, use self.padding.
389
+ :return: A zone containing the added elements.
390
+ """
290
391
 
291
392
  if padding is None:
292
393
  padding = self.padding
@@ -315,7 +416,10 @@ class DefaultLayoutA4(Zones):
315
416
 
316
417
  def check_if_overlap(self, vec:vector) -> bool:
317
418
  """
318
- Check if the vector overlaps with any existing vector in the layout.
419
+ Check if the vector overlaps with any existing vector in the page layout.
420
+
421
+ :param vec: The vector to check.
422
+ :return: True if there is an overlap, False otherwise.
319
423
  """
320
424
  for existing_vec in self['Elements'].myvectors:
321
425
  if vec.linestring.overlaps(existing_vec.linestring):
@@ -327,6 +431,7 @@ class DefaultLayoutA4(Zones):
327
431
  """
328
432
  Get the useful part of the page, excluding margins.
329
433
  """
434
+
330
435
  vec = self[('Page', _('Page'))]
331
436
  vec.find_minmax()
332
437
 
@@ -355,6 +460,7 @@ class DefaultLayoutA4(Zones):
355
460
  """
356
461
  Get the dimensions of the page in centimeters.
357
462
  """
463
+
358
464
  vec = self[('Page', _('Page'))]
359
465
  vec.find_minmax()
360
466
  width = vec.xmax - vec.xmin
@@ -364,25 +470,47 @@ class DefaultLayoutA4(Zones):
364
470
  @property
365
471
  def keys(self) -> list[str]:
366
472
  """
367
- Get the keys of the layout.
473
+ Get the keys of the page layout.
368
474
  """
475
+
369
476
  return [vec.myname for vec in self['Elements'].myvectors]
370
477
 
371
- def to_dict(self):
478
+ @cached_property
479
+ def layout(self) -> dict:
480
+ """
481
+ Get the layout as a dictionary.
482
+ """
483
+
484
+ return self._to_dict()
485
+
486
+ def get_item_in_layout(self, name:str):
487
+ """ Override the indexing to access layout elements by name. """
488
+ if isinstance(name, str):
489
+ if name not in self.layout:
490
+ raise KeyError(f"Element '{name}' not found in layout.")
491
+
492
+ return self.layout[name]
493
+
494
+ def _to_dict(self):
372
495
  """
373
496
  Convert the layout Zones to a dictionary.
374
497
  """
498
+
375
499
  layout = {}
376
500
 
377
501
  for vec in self['Elements'].myvectors:
378
502
  vec.find_minmax()
379
503
  layout[vec.myname] = rect_cm(vec.xmin, PAGE_HEIGHT - vec.ymax, vec.xmax - vec.xmin, vec.ymax - vec.ymin)
380
504
 
505
+ return layout
506
+
381
507
  def plot(self, scale=1.):
382
508
  """
383
- Plot the layout using matplotlib.
509
+ Plot the page layout using matplotlib.
510
+
384
511
  :param scale: Scale factor for the plot.
385
512
  """
513
+
386
514
  fig, ax = plt.subplots(figsize=(cm2inches(PAGE_WIDTH) * scale, cm2inches(PAGE_HEIGHT)*scale))
387
515
 
388
516
  self['Elements'].plot_matplotlib(ax = ax)
@@ -401,17 +529,6 @@ class DefaultLayoutA4(Zones):
401
529
 
402
530
  return fig, ax
403
531
 
404
- def _create_layout_pdf(self) -> dict:
405
- """
406
- Create the layout dictionary for the report.
407
- :return: A dictionary with layout information.
408
- """
409
- for vec in self['Elements'].myvectors:
410
- vec.find_minmax()
411
- self._layout[vec.myname] = rect_cm(vec.xmin, PAGE_HEIGHT - vec.ymax, vec.xmax - vec.xmin, vec.ymax - vec.ymin)
412
-
413
- return self._layout
414
-
415
532
  def _summary_versions(self):
416
533
  """ Find the versions of the simulation, wolfhece and the wolfgpu package """
417
534
  import json
@@ -426,24 +543,23 @@ class DefaultLayoutA4(Zones):
426
543
  return group_title, text
427
544
 
428
545
  def _insert_to_page(self, page: pdf.Page):
546
+ """ Insert the layout into the PDF page. """
429
547
 
430
- layout = self._create_layout_pdf()
431
-
432
- page.insert_htmlbox(layout['Title'], f"<h1>{self.title}</h1>",
548
+ page.insert_htmlbox(self.layout['Title'], f"<h1>{self.title}</h1>",
433
549
  css='h1 {font-size:16pt; font-family:Helvetica; color:#333}')
434
550
 
435
551
  # versions box
436
552
  try:
437
553
  text = self._summary_versions()
438
554
  html, css = list_to_html_aligned(text[1], font_size="10pt", font_family="Helvetica")
439
- spare_height, scale = page.insert_htmlbox(layout['Versions'], html, css=css, scale_low = 0.1)
555
+ spare_height, scale = page.insert_htmlbox(self.layout['Versions'], html, css=css, scale_low=0.1)
440
556
 
441
557
  if spare_height < 0.:
442
558
  logging.warning("Text overflow in versions box. Adjusting scale.")
443
559
  except:
444
560
  logging.error("Failed to insert versions text. Using fallback method.")
445
561
 
446
- rect = layout['Logo']
562
+ rect = self.layout['Logo']
447
563
  # Add the logo to the top-right corner
448
564
  logo_path = Path(__file__).parent / 'wolf_report.png'
449
565
  if logo_path.exists():
@@ -454,7 +570,7 @@ class DefaultLayoutA4(Zones):
454
570
  # Footer
455
571
  # ------
456
572
  # Insert the date and time of the report generation, the user and the PC name
457
- footer_rect = layout['Footer']
573
+ footer_rect = self.layout['Footer']
458
574
  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> \
459
575
  This report does not guarantee the quality of the model and in no way commits the software developers.</p>"
460
576
  page.insert_htmlbox(footer_rect, footer_text,
@@ -490,6 +606,7 @@ class DefaultLayoutA4(Zones):
490
606
  self._pdf_path = None
491
607
  return
492
608
 
609
+
493
610
  @property
494
611
  def pdf_path(self):
495
612
  """ Return the PDF document """
@@ -9,8 +9,9 @@ import pymupdf as pdf
9
9
  import wx
10
10
  from tqdm import tqdm
11
11
  from matplotlib import use, get_backend
12
+ from PIL.Image import Image
12
13
 
13
- from .common import A4_rect, rect_cm, list_to_html, list_to_html_aligned, get_rect_from_text
14
+ from .common import A4_rect, rect_cm, list_to_html, list_to_html_aligned, get_rect_from_text, convert_report_to_images
14
15
  from .common import inches2cm, pts2cm, cm2pts, cm2inches, DefaultLayoutA4, NamedTemporaryFile, pt2inches, TemporaryDirectory
15
16
  from ..wolf_array import WolfArray, header_wolf, vector, zone, Zones, wolfvertex as wv, wolfpalette
16
17
  from ..PyTranslate import _
@@ -39,11 +40,11 @@ class ArrayDifferenceLayout(DefaultLayoutA4):
39
40
  width = useful.xmax - useful.xmin
40
41
  height = useful.ymax - useful.ymin
41
42
 
42
- self._hitograms = self.add_element_repeated(_("Histogram"), width=width, height=2.5,
43
+ self._hitograms = self.add_element_repeated("Histogram", width=width, height=2.5,
43
44
  first_x=useful.xmin, first_y=useful.ymax,
44
45
  count_x=1, count_y=-2, padding=0.5)
45
46
 
46
- self._arrays = self.add_element_repeated(_("Arrays"), width= (width-self.padding) / 2, height=5.5,
47
+ self._arrays = self.add_element_repeated("Arrays", width= (width-self.padding) / 2, height=5.5,
47
48
  first_x=useful.xmin, first_y=self._hitograms.ymin - self.padding,
48
49
  count_x=2, count_y=-3, padding=0.5)
49
50
 
@@ -73,10 +74,10 @@ class CompareArraysLayout2(DefaultLayoutA4):
73
74
  width = useful.xmax - useful.xmin
74
75
  height = useful.ymax - useful.ymin
75
76
 
76
- self._summary = self.add_element_repeated(_("Histogram"), width=(width-self.padding) / 2, height=6., first_x=useful.xmin, first_y=useful.ymax-6, count_x=2, count_y=1)
77
+ self._summary = self.add_element_repeated("Histogram", width=(width-self.padding) / 2, height=6., first_x=useful.xmin, first_y=useful.ymax-6, count_x=2, count_y=1)
77
78
 
78
- self._arrays = self.add_element_repeated(_("Arrays"), width= (width-self.padding) / 2, height=6., count_x=2, count_y=1, first_x=useful.xmin, first_y=14)
79
- self._diff_rect = self.add_element(_("Position"), width= width, height=11.5, x=useful.xmin, y=useful.ymin)
79
+ self._arrays = self.add_element_repeated("Arrays", width= (width-self.padding) / 2, height=6., count_x=2, count_y=1, first_x=useful.xmin, first_y=14)
80
+ self._diff_rect = self.add_element("Position", width= width, height=11.5, x=useful.xmin, y=useful.ymin)
80
81
 
81
82
 
82
83
  class ArrayDifference():
@@ -402,12 +403,12 @@ class ArrayDifference():
402
403
 
403
404
  return fig, ax
404
405
 
405
- def _complete_report(self, layout:ArrayDifferenceLayout):
406
+ def _complete_report(self, page:ArrayDifferenceLayout):
406
407
 
407
408
  """
408
409
  Complete the report with the arrays and histograms.
409
410
  """
410
- useful = layout.useful_part
411
+ useful = page.useful_part
411
412
 
412
413
  # Plot reference array
413
414
  key_fig = [('Histogram_0-0', self.plot_histograms),
@@ -418,11 +419,11 @@ class ArrayDifference():
418
419
  ('Arrays_1-1', self.plot_to_compare),
419
420
  ('Arrays_0-2', self.plot_difference),]
420
421
 
421
- keys = layout.keys
422
+ keys = page.keys
422
423
  for key, fig_routine in key_fig:
423
424
  if key in keys:
424
425
 
425
- rect = layout._layout[key]
426
+ rect = page.get_item_in_layout(key)
426
427
 
427
428
  fig, ax = fig_routine()
428
429
 
@@ -435,7 +436,7 @@ class ArrayDifference():
435
436
  # convert canvas to PNG and insert it into the PDF
436
437
  temp_file = NamedTemporaryFile(delete=False, suffix='.png')
437
438
  fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
438
- layout._page.insert_image(layout._layout[key], filename = temp_file.name)
439
+ page._page.insert_image(page.get_item_in_layout(key), filename=temp_file.name)
439
440
  # delete the temporary file
440
441
  temp_file.delete = True
441
442
  temp_file.close()
@@ -448,7 +449,7 @@ class ArrayDifference():
448
449
  key = 'Arrays_1-2'
449
450
  if key in keys:
450
451
  text, css = list_to_html(self._summary_text, font_size='8pt')
451
- layout._page.insert_htmlbox(layout._layout[key], text,
452
+ page._page.insert_htmlbox(page.get_item_in_layout(key), text,
452
453
  css=css)
453
454
 
454
455
  def create_report(self, output_file: str | Path = None) -> Path:
@@ -461,10 +462,10 @@ class ArrayDifference():
461
462
  if output_file.exists():
462
463
  logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
463
464
 
464
- layout = ArrayDifferenceLayout(f"Differences - Index n°{self.index}")
465
- layout.create_report()
466
- self._complete_report(layout)
467
- layout.save_report(output_file)
465
+ page = ArrayDifferenceLayout(f"Differences - Index n°{self.index}")
466
+ page.create_report()
467
+ self._complete_report(page)
468
+ page.save_report(output_file)
468
469
  sleep(0.2) # Ensure the file is saved before returning
469
470
 
470
471
  return output_file
@@ -788,18 +789,18 @@ class CompareArrays:
788
789
  ]
789
790
  return text_left, text_right
790
791
 
791
- def _complete_report(self, layout:CompareArraysLayout):
792
+ def _complete_report(self, page:CompareArraysLayout):
792
793
  """ Complete the report with the global summary and individual differences. """
793
794
 
794
795
  key_fig = [('Arrays_0-0', self.plot_reference),
795
796
  ('Arrays_1-0', self.plot_to_compare),
796
797
  ('Difference', self.plot_difference),]
797
798
 
798
- keys = layout.keys
799
+ keys = page.keys
799
800
  for key, fig_routine in key_fig:
800
801
  if key in keys:
801
802
 
802
- rect = layout._layout[key]
803
+ rect = page.get_item_in_layout(key)
803
804
 
804
805
  fig, ax = fig_routine()
805
806
 
@@ -809,7 +810,7 @@ class CompareArrays:
809
810
  # convert canvas to PNG and insert it into the PDF
810
811
  temp_file = NamedTemporaryFile(delete=False, suffix='.png')
811
812
  fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
812
- layout._page.insert_image(layout._layout[key], filename=temp_file.name)
813
+ page._page.insert_image(page.get_item_in_layout(key), filename=temp_file.name)
813
814
  # delete the temporary file
814
815
  temp_file.delete = True
815
816
  temp_file.close()
@@ -821,12 +822,12 @@ class CompareArrays:
821
822
 
822
823
  tleft, tright = self.summary_text
823
824
 
824
- rect = layout._layout['Summary_0-0']
825
+ rect = page.get_item_in_layout('Summary_0-0')
825
826
  text_left, css_left = list_to_html(tleft, font_size='8pt')
826
- layout._page.insert_htmlbox(rect, text_left, css=css_left)
827
- rect = layout._layout['Summary_1-0']
827
+ page._page.insert_htmlbox(rect, text_left, css=css_left)
828
+ rect = page.get_item_in_layout('Summary_1-0')
828
829
  text_right, css_right = list_to_html(tright, font_size='8pt')
829
- layout._page.insert_htmlbox(rect, text_right, css=css_right)
830
+ page._page.insert_htmlbox(rect, text_right, css=css_right)
830
831
 
831
832
  def plot_histogram_features(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
832
833
  """
@@ -901,7 +902,7 @@ class CompareArrays:
901
902
 
902
903
  return fig, ax
903
904
 
904
- def _complete_report2(self, layout:CompareArraysLayout):
905
+ def _complete_report2(self, page:CompareArraysLayout):
905
906
  """ Complete the report with the individual differences. """
906
907
 
907
908
  key_fig = [('Histogram_0-0', self.plot_histogram_features),
@@ -911,11 +912,11 @@ class CompareArrays:
911
912
  ('Position', self.plot_topo_grey),
912
913
  ]
913
914
 
914
- keys = layout.keys
915
+ keys = page.keys
915
916
  for key, fig_routine in key_fig:
916
917
  if key in keys:
917
918
 
918
- rect = layout._layout[key]
919
+ rect = page.get_item_in_layout(key)
919
920
 
920
921
  fig, ax = fig_routine()
921
922
 
@@ -926,7 +927,7 @@ class CompareArrays:
926
927
  # convert canvas to PNG and insert it into the PDF
927
928
  temp_file = NamedTemporaryFile(delete=False, suffix='.png')
928
929
  fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
929
- layout._page.insert_image(layout._layout[key], filename=temp_file.name)
930
+ page._page.insert_image(page.get_item_in_layout(key), filename=temp_file.name)
930
931
  # delete the temporary file
931
932
  temp_file.delete = True
932
933
  temp_file.close()
@@ -949,9 +950,9 @@ class CompareArrays:
949
950
  if output_file.exists():
950
951
  logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
951
952
 
952
- layout = CompareArraysLayout("Comparison Report")
953
- layout.create_report()
954
- self._complete_report(layout)
953
+ page = CompareArraysLayout("Comparison Report")
954
+ page.create_report()
955
+ self._complete_report(page)
955
956
 
956
957
  if nb_max_differences < 0:
957
958
  nb_max_differences = len(self.difference_parts)
@@ -975,12 +976,12 @@ class CompareArrays:
975
976
  all_pdfs.extend([self.difference_parts[idx].create_report(Path(temp_dir) / f"array_difference_{idx}.pdf") for idx in tqdm(features_to_treat, desc="Creating individual difference reports")])
976
977
 
977
978
  for pdf_file in tqdm(all_pdfs, desc="Compiling PDFs"):
978
- layout._doc.insert_file(pdf_file)
979
+ page._doc.insert_file(pdf_file)
979
980
 
980
981
  # create a TOC
981
- layout._doc.set_toc(layout._doc.get_toc())
982
+ page._doc.set_toc(page._doc.get_toc())
982
983
 
983
- layout.save_report(output_file)
984
+ page.save_report(output_file)
984
985
  self._pdf_path = output_file
985
986
 
986
987
  @property
@@ -991,6 +992,14 @@ class CompareArrays:
991
992
  else:
992
993
  raise AttributeError("PDF path not set. Please create the report first.")
993
994
 
995
+ def convert_report_to_images(self, dpi:int = 150) -> list[Image]:
996
+ """ Convert the PDF report to a list of images (one per page). """
997
+ if self.pdf_path is None or not self.pdf_path.exists():
998
+ raise FileNotFoundError("PDF report not found. Please create the report first.")
999
+
1000
+ images = convert_report_to_images(self.pdf_path, dpi=dpi)
1001
+ return images
1002
+
994
1003
  class CompareArrays_wx(PDFViewer):
995
1004
 
996
1005
  def __init__(self, reference: WolfArray | str | Path,