fiqus 2026.1.0__py3-none-any.whl → 2026.1.2__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 (42) hide show
  1. fiqus/MainFiQuS.py +1 -8
  2. fiqus/data/DataConductor.py +4 -8
  3. fiqus/data/DataFiQuSMultipole.py +358 -167
  4. fiqus/data/DataModelCommon.py +30 -15
  5. fiqus/data/DataMultipole.py +33 -10
  6. fiqus/data/DataWindingsCCT.py +37 -37
  7. fiqus/data/RegionsModelFiQuS.py +1 -1
  8. fiqus/geom_generators/GeometryMultipole.py +751 -54
  9. fiqus/getdp_runners/RunGetdpMultipole.py +181 -31
  10. fiqus/mains/MainMultipole.py +109 -17
  11. fiqus/mesh_generators/MeshCCT.py +209 -209
  12. fiqus/mesh_generators/MeshMultipole.py +938 -263
  13. fiqus/parsers/ParserCOND.py +2 -1
  14. fiqus/parsers/ParserDAT.py +16 -16
  15. fiqus/parsers/ParserGetDPOnSection.py +212 -212
  16. fiqus/parsers/ParserGetDPTimeTable.py +134 -134
  17. fiqus/parsers/ParserMSH.py +53 -53
  18. fiqus/parsers/ParserRES.py +142 -142
  19. fiqus/plotters/PlotPythonCCT.py +133 -133
  20. fiqus/plotters/PlotPythonMultipole.py +18 -18
  21. fiqus/post_processors/PostProcessMultipole.py +16 -6
  22. fiqus/pre_processors/PreProcessCCT.py +175 -175
  23. fiqus/pro_assemblers/ProAssembler.py +3 -3
  24. fiqus/pro_material_functions/ironBHcurves.pro +246 -246
  25. fiqus/pro_templates/combined/CC_Module.pro +1213 -0
  26. fiqus/pro_templates/combined/ConductorAC_template.pro +1025 -0
  27. fiqus/pro_templates/combined/Multipole_template.pro +2738 -1338
  28. fiqus/pro_templates/combined/TSA_materials.pro +102 -2
  29. fiqus/pro_templates/combined/materials.pro +54 -3
  30. fiqus/utils/Utils.py +18 -25
  31. fiqus/utils/update_data_settings.py +1 -1
  32. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/METADATA +64 -68
  33. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/RECORD +42 -40
  34. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/WHEEL +1 -1
  35. tests/test_geometry_generators.py +29 -32
  36. tests/test_mesh_generators.py +35 -34
  37. tests/test_solvers.py +32 -31
  38. tests/utils/fiqus_test_classes.py +396 -147
  39. tests/utils/generate_reference_files_ConductorAC.py +57 -57
  40. tests/utils/helpers.py +76 -1
  41. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/LICENSE.txt +0 -0
  42. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/top_level.txt +0 -0
@@ -4,13 +4,15 @@ import filecmp
4
4
  import gmsh
5
5
  import logging
6
6
  import shutil
7
- from typing import Union, Tuple, Literal
7
+ from typing import Union, Tuple, Literal, List
8
8
  import ruamel.yaml
9
9
  import numpy as np
10
+ import csv
10
11
 
11
12
  from fiqus.data.DataFiQuS import FDM
12
13
  from fiqus.MainFiQuS import MainFiQuS
13
14
  from fiqus.utils.Utils import GmshUtils
15
+ from fiqus.parsers.ParserCOND import ParserCOND
14
16
  from fiqus.parsers.ParserMSH import ParserMSH
15
17
  from fiqus.utils.Utils import FilesAndFolders as FFs
16
18
 
@@ -80,21 +82,21 @@ class BaseClassesForTests(unittest.TestCase):
80
82
  return fdm
81
83
 
82
84
  def run_fiqus(
83
- self,
84
- data_model: FDM,
85
- model_name: str,
86
- run_type: Literal[
87
- "start_from_yaml",
88
- "mesh_only",
89
- "geometry_only",
90
- "geometry_and_mesh",
91
- "pre_process_only",
92
- "mesh_and_solve_with_post_process_python",
93
- "solve_with_post_process_python",
94
- "solve_only",
95
- "post_process_getdp_only",
96
- "post_process_python_only",
97
- ],
85
+ self,
86
+ data_model: FDM,
87
+ model_name: str,
88
+ run_type: Literal[
89
+ "start_from_yaml",
90
+ "mesh_only",
91
+ "geometry_only",
92
+ "geometry_and_mesh",
93
+ "pre_process_only",
94
+ "mesh_and_solve_with_post_process_python",
95
+ "solve_with_post_process_python",
96
+ "solve_only",
97
+ "post_process_getdp_only",
98
+ "post_process_python_only",
99
+ ],
98
100
  ) -> None:
99
101
  """
100
102
  This method runs FiQuS with the given model name and run type.
@@ -128,32 +130,59 @@ class BaseClassesForTests(unittest.TestCase):
128
130
  model_folder = os.path.join(self.outputs_folder, f"{model_name}_{run_type}")
129
131
  FFs.prep_folder(model_folder)
130
132
 
133
+ reference_geometry_folder = self.get_path_to_specific_reference_folder(
134
+ data_model, model_name, folder="Geometry"
135
+ )
136
+ reference_mesh_folder = self.get_path_to_specific_reference_folder(
137
+ data_model, model_name, folder="Mesh"
138
+ )
139
+ reference_solution_folder = self.get_path_to_specific_reference_folder(
140
+ data_model, model_name, folder="Solution"
141
+ )
142
+
143
+ output_geometry_folder = self.get_path_to_specific_output_folder(
144
+ data_model, model_name, run_type=run_type, folder="Geometry"
145
+ )
146
+ output_mesh_folder = self.get_path_to_specific_output_folder(
147
+ data_model, model_name, run_type=run_type, folder="Mesh"
148
+ )
149
+ output_solution_folder = self.get_path_to_specific_output_folder(
150
+ data_model, model_name, run_type=run_type, folder="Solution"
151
+ )
152
+
131
153
  # Depending on the run_type, copy the reference files to the model folder:
132
- if run_type in ["geometry_only", "geometry_and_mesh", "start_from_yaml"]:
133
- pass
134
- else:
135
- # Copy the reference geometry and mesh folders to the model folder:
136
- reference_geometry_folder = self.get_path_to_specific_reference_folder(
137
- data_model, model_name, folder="Geometry"
154
+ if run_type in ["geometry_only"]:
155
+ pass # do not copy anything
156
+ elif run_type in ["mesh_only", 'mesh_and_solve_with_post_process_python']:
157
+ shutil.copytree(
158
+ reference_geometry_folder, output_geometry_folder, dirs_exist_ok=True,
159
+ ignore=shutil.ignore_patterns('Mesh*')
138
160
  )
139
- reference_mesh_folder = self.get_path_to_specific_reference_folder(
140
- data_model, model_name, folder="Mesh"
161
+ elif run_type in ["solve_only", "solve_with_post_process_python"]:
162
+ shutil.copytree(
163
+ reference_geometry_folder, output_geometry_folder, dirs_exist_ok=True,
164
+ ignore=shutil.ignore_patterns('Mesh*')
141
165
  )
142
-
143
- output_geometry_folder = self.get_path_to_specific_output_folder(
144
- data_model, model_name, run_type=run_type, folder="Geometry"
166
+ shutil.copytree(
167
+ reference_mesh_folder, output_mesh_folder, dirs_exist_ok=True,
168
+ ignore=shutil.ignore_patterns('Solution*')
145
169
  )
146
- output_mesh_folder = self.get_path_to_specific_output_folder(
147
- data_model, model_name, run_type=run_type, folder="Mesh"
170
+ elif run_type in ['post_process_python_only', 'post_process_getdp_only']:
171
+ shutil.copytree(
172
+ reference_geometry_folder, output_geometry_folder, dirs_exist_ok=True,
173
+ ignore=shutil.ignore_patterns('Mesh*')
148
174
  )
149
-
150
175
  shutil.copytree(
151
- reference_geometry_folder, output_geometry_folder, dirs_exist_ok=True, ignore=shutil.ignore_patterns('Mesh*')
176
+ reference_mesh_folder, output_mesh_folder, dirs_exist_ok=True,
177
+ ignore=shutil.ignore_patterns('Solution*')
152
178
  )
153
179
  shutil.copytree(
154
- reference_mesh_folder, output_mesh_folder, dirs_exist_ok=True, ignore=shutil.ignore_patterns('Solution*')
180
+ reference_solution_folder, output_solution_folder, dirs_exist_ok=True, ignore=None
181
+ )
182
+ else: # "geometry_and_mesh", "start_from_yaml"
183
+ raise ValueError(
184
+ f"The test can not be run at the moment with run type set to {run_type}"
155
185
  )
156
-
157
186
  # Run FiQuS:
158
187
  if hasattr(self, "getdp_path"):
159
188
  MainFiQuS(
@@ -172,23 +201,24 @@ class BaseClassesForTests(unittest.TestCase):
172
201
  )
173
202
 
174
203
  def get_path_to_generated_file(
175
- self,
176
- data_model: FDM,
177
- model_name: str,
178
- file_name: str,
179
- file_extension: str,
180
- run_type: Literal[
181
- "start_from_yaml",
182
- "mesh_only",
183
- "geometry_only",
184
- "geometry_and_mesh",
185
- "pre_process_only",
186
- "mesh_and_solve_with_post_process_python",
187
- "solve_with_post_process_python",
188
- "solve_only",
189
- "post_process_getdp_only",
190
- "post_process_python_only",
191
- ],
204
+ self,
205
+ data_model: FDM,
206
+ model_name: str,
207
+ file_name: str,
208
+ file_extension: str,
209
+ run_type: Literal[
210
+ "start_from_yaml",
211
+ "mesh_only",
212
+ "geometry_only",
213
+ "geometry_and_mesh",
214
+ "pre_process_only",
215
+ "mesh_and_solve_with_post_process_python",
216
+ "solve_with_post_process_python",
217
+ "solve_only",
218
+ "post_process_getdp_only",
219
+ "post_process_python_only",
220
+ ],
221
+ subfolder=None
192
222
  ) -> Union[str, os.PathLike]:
193
223
  """
194
224
  This method returns the path to the generated file with the given extension
@@ -200,6 +230,8 @@ class BaseClassesForTests(unittest.TestCase):
200
230
  :type file_name: str
201
231
  :param file_extension: file extension of the generated file
202
232
  :type file_extension: str
233
+ :param subfolder: additional folder inside specified 'folder'
234
+ :type subfolder: str
203
235
  :param run_type: run type to run FiQuS with
204
236
  :type run_type: Literal[
205
237
  "start_from_yaml",
@@ -222,20 +254,29 @@ class BaseClassesForTests(unittest.TestCase):
222
254
  folder = "Mesh"
223
255
  elif run_type == "solve_only":
224
256
  folder = "Solution"
257
+ elif run_type == "solve_with_post_process_python":
258
+ folder = "Solution"
259
+ elif run_type == "post_process_python_only":
260
+ folder = "Solution"
225
261
  else:
226
262
  raise ValueError(
227
- "The run type must be geometry_only, mesh_only, or solve_only!"
263
+ "The run type must be geometry_only, mesh_only, solve_only, solve_with_post_process_python, post_process_python_only!"
228
264
  )
229
265
 
230
266
  sections_folder = self.get_path_to_specific_output_folder(
231
267
  data_model, model_name, run_type, folder
232
268
  )
233
269
 
234
- generated_file = os.path.join(
235
- sections_folder,
236
- f"{file_name}.{file_extension}",
237
- )
238
-
270
+ if subfolder:
271
+ generated_file = os.path.join(
272
+ sections_folder, subfolder,
273
+ f"{file_name}.{file_extension}",
274
+ )
275
+ else:
276
+ generated_file = os.path.join(
277
+ sections_folder,
278
+ f"{file_name}.{file_extension}",
279
+ )
239
280
  # Check if the file exists:
240
281
  if not os.path.isfile(generated_file):
241
282
  raise FileNotFoundError(
@@ -245,14 +286,14 @@ class BaseClassesForTests(unittest.TestCase):
245
286
  return generated_file
246
287
 
247
288
  def get_path_to_specific_reference_folder(
248
- self,
249
- data_model: FDM,
250
- model_name: str,
251
- folder: Literal[
252
- "Geometry",
253
- "Mesh",
254
- "Solution",
255
- ],
289
+ self,
290
+ data_model: FDM,
291
+ model_name: str,
292
+ folder: Literal[
293
+ "Geometry",
294
+ "Mesh",
295
+ "Solution",
296
+ ],
256
297
  ):
257
298
  """
258
299
  This method returns the path to a specific reference folder (Geometry, Mesh, or
@@ -295,26 +336,26 @@ class BaseClassesForTests(unittest.TestCase):
295
336
  return reference_folder
296
337
 
297
338
  def get_path_to_specific_output_folder(
298
- self,
299
- data_model: FDM,
300
- model_name: str,
301
- run_type: Literal[
302
- "start_from_yaml",
303
- "mesh_only",
304
- "geometry_only",
305
- "geometry_and_mesh",
306
- "pre_process_only",
307
- "mesh_and_solve_with_post_process_python",
308
- "solve_with_post_process_python",
309
- "solve_only",
310
- "post_process_getdp_only",
311
- "post_process_python_only",
312
- ],
313
- folder: Literal[
314
- "Geometry",
315
- "Mesh",
316
- "Solution",
317
- ],
339
+ self,
340
+ data_model: FDM,
341
+ model_name: str,
342
+ run_type: Literal[
343
+ "start_from_yaml",
344
+ "mesh_only",
345
+ "geometry_only",
346
+ "geometry_and_mesh",
347
+ "pre_process_only",
348
+ "mesh_and_solve_with_post_process_python",
349
+ "solve_with_post_process_python",
350
+ "solve_only",
351
+ "post_process_getdp_only",
352
+ "post_process_python_only",
353
+ ],
354
+ folder: Literal[
355
+ "Geometry",
356
+ "Mesh",
357
+ "Solution",
358
+ ],
318
359
  ):
319
360
  """
320
361
  This method returns a specific path (Geometry, Mesh, or Solution) to the output
@@ -377,16 +418,17 @@ class BaseClassesForTests(unittest.TestCase):
377
418
  return output_folder
378
419
 
379
420
  def get_path_to_reference_file(
380
- self,
381
- data_model: FDM,
382
- model_name: str,
383
- file_name: str,
384
- file_extension: str,
385
- folder: Literal[
386
- "Geometry",
387
- "Mesh",
388
- "Solution",
389
- ],
421
+ self,
422
+ data_model: FDM,
423
+ model_name: str,
424
+ file_name: str,
425
+ file_extension: str,
426
+ folder: Literal[
427
+ "Geometry",
428
+ "Mesh",
429
+ "Solution",
430
+ ],
431
+ subfolder=None
390
432
  ) -> Union[str, os.PathLike]:
391
433
  """
392
434
  This method returns the path to the reference file with the given extension
@@ -404,26 +446,32 @@ class BaseClassesForTests(unittest.TestCase):
404
446
  "Mesh",
405
447
  "Solution",
406
448
  ]
449
+ :param subfolder: additional folder inside specified 'folder'
450
+ :type subfolder: str
407
451
  :return: path to the reference file
408
452
  :rtype: Union[str, os.PathLike]
409
453
  """
410
454
  reference_folder = self.get_path_to_specific_reference_folder(
411
455
  data_model, model_name, folder=folder
412
456
  )
413
- reference_file = os.path.join(
414
- reference_folder,
415
- f"{file_name}.{file_extension}",
416
- )
457
+ if subfolder:
458
+ reference_file = os.path.join(
459
+ reference_folder, subfolder,
460
+ f"{file_name}.{file_extension}", )
461
+ else:
462
+ reference_file = os.path.join(
463
+ reference_folder,
464
+ f"{file_name}.{file_extension}", )
417
465
 
418
466
  # Check if the file exists:
419
467
  if not os.path.isfile(reference_file):
420
468
  raise FileNotFoundError(
421
- f"Could not find the reference file: {file_name}.{file_extension}!"
469
+ f"Could not find the reference file: {reference_file}!"
422
470
  )
423
471
 
424
472
  return reference_file
425
473
 
426
- def compare_json_or_yaml_files(self, file_1, file_2, tolerance=0,excluded_keys=None):
474
+ def compare_json_or_yaml_files(self, file_1, file_2, tolerance=0, excluded_keys=None):
427
475
  """
428
476
  This method compares the contents of two JSON or YAML files. It is used to
429
477
  check that the generated files are the same as the reference.
@@ -463,6 +511,33 @@ class BaseClassesForTests(unittest.TestCase):
463
511
  else:
464
512
  self.compare_dicts(file_1_dictionary, file_2_dictionary, tolerance)
465
513
 
514
+ def compare_conductor_files(self, file_1, file_2, tolerance=0):
515
+ """
516
+ This method compares the contents of two JSON or YAML files. It is used to
517
+ check that the generated files are the same as the reference.
518
+
519
+ :param file_1: path to the first file
520
+ :type file_1: Union[str, os.PathLike]
521
+ :param file_2: path to the second file
522
+ :type file_2: Union[str, os.PathLike]
523
+ :param tolerance: tolerance for numeric differences (default is 0)
524
+ :type tolerance: float
525
+ """
526
+ print('Comparing:')
527
+ print(f'Output file: {file_1}')
528
+ print(f'Reference file: {file_2}')
529
+ file_1_dictionary = ParserCOND().read_cond(file_1)
530
+ file_2_dictionary = ParserCOND().read_cond(file_2)
531
+
532
+ # Compare the dictionaries:
533
+ if tolerance == 0:
534
+ self.assertDictEqual(
535
+ file_1_dictionary,
536
+ file_2_dictionary,
537
+ msg=f"{file_1} did not match {file_2}!",
538
+ )
539
+ else:
540
+ self.compare_dicts(file_1_dictionary, file_2_dictionary, tolerance)
466
541
 
467
542
  def _remove_excluded_keys(self, data, excluded_keys):
468
543
  """
@@ -501,20 +576,27 @@ class BaseClassesForTests(unittest.TestCase):
501
576
  self.fail(f'Key "{key}" not in both {dict1} and {dict2}')
502
577
  if isinstance(dict1[key], dict):
503
578
  self.compare_dicts(dict1[key], dict2[key], tolerance)
504
- elif isinstance(dict1[key], float): # To handle precision errors in floats
579
+ elif isinstance(dict1[key], float): # To handle precision errors in floats
505
580
  if not np.isclose(dict1[key], dict2[key], atol=tolerance):
506
581
  self.fail(f'Values for key {key} are not close: {dict1[key]} vs {dict2[key]}')
507
- elif isinstance(dict1[key], list): # To handle precision errors in lists of floats
582
+ elif isinstance(dict1[key], list): # To handle precision errors in lists of floats
508
583
  if len(dict1[key]) != len(dict2[key]):
509
584
  self.fail(f'Lists for key {key} are not the same length')
510
585
  for i in range(len(dict1[key])):
511
586
  if isinstance(dict1[key][i], float):
512
587
  if not np.isclose(dict1[key][i], dict2[key][i], atol=tolerance):
513
- self.fail(f'Values at index {i} for key {key} are not close: {dict1[key][i]} vs {dict2[key][i]}')
588
+ self.fail(
589
+ f'Values at index {i} for key {key} are not close: {dict1[key][i]} vs {dict2[key][i]}')
514
590
  elif dict1[key][i] != dict2[key][i]:
515
- self.fail(f'Values at index {i} for key {key} are not equal: {dict1[key][i]} vs {dict2[key][i]}')
591
+ self.fail(
592
+ f'Values at index {i} for key {key} are not equal: {dict1[key][i]} vs {dict2[key][i]}')
516
593
  else:
517
- if dict1[key] != dict2[key]:
594
+ if dict1[key] == dict2[key]:
595
+ pass
596
+ elif isinstance(dict1[key], str): # To handle precision errors in floats
597
+ if not np.isclose(float(dict1[key]), float(dict2[key]), atol=tolerance):
598
+ self.fail(f'Values for key {key} are not close: {float(dict1[key])} vs {float(dict2[key])}')
599
+ else:
518
600
  self.fail(f'Values for key {key} are not equal: {dict1[key]} vs {dict2[key]}')
519
601
 
520
602
  def compare_pkl_files(self, file_1, file_2):
@@ -532,8 +614,9 @@ class BaseClassesForTests(unittest.TestCase):
532
614
  filecmp.cmp(file_1, file_2),
533
615
  msg=f"{file_1} did not match {file_2}!",
534
616
  )
535
-
536
- def filter_content(self, file_path, keywords, n):
617
+
618
+ @staticmethod
619
+ def filter_content(file_path, keywords, n):
537
620
  """
538
621
  Read a file and return its content as a string,
539
622
  excluding lines containing any of the specified keywords.
@@ -547,22 +630,53 @@ class BaseClassesForTests(unittest.TestCase):
547
630
  # Filter remaining lines
548
631
  return ''.join(line for line in f if not any(keyword in line for keyword in keywords))
549
632
 
550
-
551
633
  def compare_text_files(self, file_1, file_2, exclude_lines_keywords: list = None, exclude_first_n_lines: int = 0):
552
634
  """
553
- This method compares the contents of two files. It is used to check that the
554
- generated files are the same as the reference.
635
+ This method compares the contents of two files, normalizing the text to ignore
636
+ whitespaces and line skips. It is used to check that the generated files are the
637
+ same as the reference.
555
638
 
556
639
  :param file_1: path to the first file
557
640
  :type file_1: Union[str, os.PathLike]
558
641
  :param file_2: path to the second file
559
642
  :type file_2: Union[str, os.PathLike]
643
+ :param exclude_lines_keywords: List of keywords to exclude lines containing them.
644
+ :type exclude_lines_keywords: list
645
+ :param exclude_first_n_lines: Number of lines to exclude from the top.
646
+ :type exclude_first_n_lines: int
560
647
  """
561
- if exclude_lines_keywords:
562
- # more complicated check that needs to loop through the lines
563
- filtered_content1 = self.filter_content(file_1, exclude_lines_keywords, exclude_first_n_lines)
564
- filtered_content2 = self.filter_content(file_2, exclude_lines_keywords, exclude_first_n_lines)
565
- self.assertTrue(filtered_content1 == filtered_content2)
648
+ print(f'Comparing: {file_1} with {file_2}')
649
+ if exclude_lines_keywords:
650
+ # Normalize the content of both files
651
+ normalized_content_1 = self._normalize_file_content(file_1, exclude_lines_keywords, exclude_first_n_lines)
652
+ normalized_content_2 = self._normalize_file_content(file_2, exclude_lines_keywords, exclude_first_n_lines)
653
+
654
+ # Split the normalized content into lines
655
+ lines1 = normalized_content_1.splitlines()
656
+ lines2 = normalized_content_2.splitlines()
657
+
658
+ # Compare line by line and collect differences
659
+ differences = []
660
+ max_lines = max(len(lines1), len(lines2))
661
+ for i in range(max_lines):
662
+ line1 = lines1[i] if i < len(lines1) else "<No Line>"
663
+ line2 = lines2[i] if i < len(lines2) else "<No Line>"
664
+ if line1 != line2:
665
+ differences.append(
666
+ f"Line {i + 1}:\n File 1: {line1[:100]}\n File 2: {line2[:100]}"
667
+ )
668
+ break
669
+
670
+ # If there are differences, include them in the assertion message
671
+ if differences:
672
+ diff_message = "\n".join(differences)
673
+ self.assertEqual(
674
+ normalized_content_1,
675
+ normalized_content_2,
676
+ msg=f"{file_1} did not match {file_2}!\nDifferences:\n{diff_message[0:100]}"
677
+ )
678
+ else:
679
+ print("Files match!")
566
680
  else:
567
681
  # Compare the files with a binary check
568
682
  self.assertTrue(
@@ -570,12 +684,91 @@ class BaseClassesForTests(unittest.TestCase):
570
684
  msg=f"{file_1} did not match {file_2}!",
571
685
  )
572
686
 
687
+ def _normalize_file_content(self, file_path, exclude_lines_keywords, exclude_first_n_lines):
688
+ """
689
+ Normalize the content of a file by:
690
+ - Removing extra spaces and tabs
691
+ - Ignoring line breaks
692
+ - Excluding lines with specific keywords
693
+ - Skipping the first n lines
694
+
695
+ :param file_path: Path to the file to normalize
696
+ :type file_path: Union[str, os.PathLike]
697
+ :param exclude_lines_keywords: List of keywords to exclude lines containing them.
698
+ :type exclude_lines_keywords: list
699
+ :param exclude_first_n_lines: Number of lines to exclude from the top.
700
+ :type exclude_first_n_lines: int
701
+ :return: Normalized content as a single string
702
+ :rtype: str
703
+ """
704
+ with open(file_path, 'r', encoding='utf-8') as f:
705
+ # Skip the first n lines
706
+ lines = f.readlines()[exclude_first_n_lines:]
707
+
708
+ # Filter out lines containing any of the specified keywords
709
+ if exclude_lines_keywords:
710
+ lines = [line for line in lines if not any(keyword in line for keyword in exclude_lines_keywords)]
711
+
712
+ # Normalize the lines by removing extra spaces and joining them into a single string
713
+ normalized_content = ''.join(' '.join(line.split()) for line in lines)
714
+ return normalized_content
715
+
716
+ def compare_csv_files(self, file_1: Union[str, os.PathLike], file_2: Union[str, os.PathLike],
717
+ exclude_lines_keywords: List[str] = None, exclude_first_n_lines: int = 0,
718
+ tolerance: float = 0.0):
719
+ """
720
+ Compare two CSV files while allowing for numerical differences within a specified tolerance.
721
+
722
+ :param file_1: Path to the first CSV file.
723
+ :param file_2: Path to the second CSV file.
724
+ :param exclude_lines_keywords: List of keywords; lines containing these will be excluded.
725
+ :param exclude_first_n_lines: Number of lines to exclude from the top.
726
+ :param tolerance: Acceptable numerical difference between corresponding values.
727
+ """
728
+ print(f'Comparing: {file_1} with {file_2}')
729
+
730
+ # Read and filter files
731
+ filtered_content1 = self._filter_csv_content(file_1, exclude_lines_keywords, exclude_first_n_lines)
732
+ filtered_content2 = self._filter_csv_content(file_2, exclude_lines_keywords, exclude_first_n_lines)
733
+
734
+ # Check that the number of lines match
735
+ self.assertEqual(len(filtered_content1), len(filtered_content2),
736
+ msg=f"File lengths do not match: {len(filtered_content1)} vs {len(filtered_content2)}")
737
+
738
+ for row1, row2 in zip(filtered_content1, filtered_content2):
739
+ self.assertEqual(len(row1), len(row2), msg=f"Row lengths do not match: {row1} vs {row2}")
740
+ for val1, val2 in zip(row1, row2):
741
+ if self._is_number(val1) and self._is_number(val2):
742
+ if np.isnan(float(val1)) and np.isnan(float(val2)):
743
+ continue # Consider NaN values as equal
744
+ self.assertTrue(np.isclose(float(val1), float(val2), atol=tolerance),
745
+ msg=f"Values {val1} and {val2} differ beyond tolerance {tolerance} for file {file_1} and {file_2}")
746
+ else:
747
+ self.assertEqual(val1, val2, msg=f"String values do not match: {val1} vs {val2}")
748
+
749
+ def _filter_csv_content(self, file_path, exclude_lines_keywords, exclude_first_n_lines):
750
+ """Helper method to read CSV file and filter lines based on keywords and line numbers."""
751
+ with open(file_path, newline='', encoding='utf-8') as f:
752
+ reader = csv.reader(f)
753
+ content = [row for i, row in enumerate(reader)
754
+ if i >= exclude_first_n_lines and
755
+ (not exclude_lines_keywords or not any(kw in ','.join(row) for kw in exclude_lines_keywords))]
756
+ return content
757
+
758
+ def _is_number(self, value):
759
+ """Helper method to check if a value is a number."""
760
+ try:
761
+ float(value)
762
+ return True
763
+ except ValueError:
764
+ return False
765
+
573
766
 
574
767
  class FiQuSGeometryTests(BaseClassesForTests):
575
768
  def generate_geometry(
576
- self,
577
- data_model: FDM,
578
- model_name: str,
769
+ self,
770
+ data_model: FDM,
771
+ model_name: str,
579
772
  ) -> Tuple[Union[str, os.PathLike], Union[str, os.PathLike]]:
580
773
  """
581
774
  This method generates the geometry for the given model name.
@@ -591,9 +784,9 @@ class FiQuSGeometryTests(BaseClassesForTests):
591
784
  self.model_name = model_name
592
785
 
593
786
  def compare_number_of_entities(
594
- self,
595
- geometry_file_1: Union[str, os.PathLike],
596
- geometry_file_2: Union[str, os.PathLike],
787
+ self,
788
+ geometry_file_1: Union[str, os.PathLike],
789
+ geometry_file_2: Union[str, os.PathLike],
597
790
  ):
598
791
  """
599
792
  This method compares the number of entities for each dimension in two geometry
@@ -630,29 +823,29 @@ class FiQuSGeometryTests(BaseClassesForTests):
630
823
  )
631
824
 
632
825
  def get_path_to_generated_file(
633
- self, data_model: FDM, file_name: str, file_extension: str
826
+ self, data_model: FDM, model_name: str, file_extension: str
634
827
  ) -> Union[str, os.PathLike]:
635
828
  return super().get_path_to_generated_file(
636
829
  data_model,
637
830
  self.model_name,
638
- file_name,
831
+ model_name,
639
832
  file_extension,
640
833
  run_type="geometry_only",
641
834
  )
642
835
 
643
836
  def get_path_to_reference_file(
644
- self, data_model: FDM, file_name: str, file_extension: str
837
+ self, data_model: FDM, model_name: str, file_extension: str
645
838
  ) -> Union[str, os.PathLike]:
646
839
  return super().get_path_to_reference_file(
647
- data_model, self.model_name, file_name, file_extension, folder="Geometry"
840
+ data_model, self.model_name, model_name, file_extension, folder="Geometry"
648
841
  )
649
842
 
650
843
 
651
844
  class FiQuSMeshTests(BaseClassesForTests):
652
845
  def generate_mesh(
653
- self,
654
- data_model: FDM,
655
- model_name: str,
846
+ self,
847
+ data_model: FDM,
848
+ model_name: str,
656
849
  ) -> Tuple[Union[str, os.PathLike], Union[str, os.PathLike]]:
657
850
  """
658
851
  This method generates the mesh for the given model name.
@@ -666,7 +859,7 @@ class FiQuSMeshTests(BaseClassesForTests):
666
859
  self.model_name = model_name
667
860
 
668
861
  def compare_mesh_qualities(
669
- self, mesh_file_1: Union[str, os.PathLike], mesh_file_2: Union[str, os.PathLike]
862
+ self, mesh_file_1: Union[str, os.PathLike], mesh_file_2: Union[str, os.PathLike]
670
863
  ):
671
864
  """
672
865
  This method compares the mesh qualities of two mesh files. It is used to check
@@ -697,25 +890,26 @@ class FiQuSMeshTests(BaseClassesForTests):
697
890
  )
698
891
 
699
892
  def get_path_to_generated_file(
700
- self, data_model, file_name: str, file_extension: str
893
+ self, data_model, model_name: str, file_extension: str
701
894
  ) -> Union[str, os.PathLike]:
702
895
  return super().get_path_to_generated_file(
703
- data_model, self.model_name, file_name, file_extension, run_type="mesh_only"
896
+ data_model, self.model_name, model_name, file_extension, run_type="mesh_only"
704
897
  )
705
898
 
706
899
  def get_path_to_reference_file(
707
- self, data_model, file_name: str, file_extension: str
900
+ self, data_model, model_name: str, file_extension: str
708
901
  ) -> Union[str, os.PathLike]:
709
902
  return super().get_path_to_reference_file(
710
- data_model, self.model_name, file_name, file_extension, folder="Mesh"
903
+ data_model, self.model_name, model_name, file_extension, folder="Mesh"
711
904
  )
712
905
 
713
906
 
714
907
  class FiQuSSolverTests(BaseClassesForTests):
715
908
  def solve(
716
- self,
717
- data_model: FDM,
718
- model_name: str,
909
+ self,
910
+ data_model: FDM,
911
+ model_name: str,
912
+ run_type: str = "solve_only"
719
913
  ) -> Tuple[Union[str, os.PathLike], Union[str, os.PathLike]]:
720
914
  """
721
915
  This method solves the given model name.
@@ -725,11 +919,12 @@ class FiQuSSolverTests(BaseClassesForTests):
725
919
  :param model_name: name of the model to generate the mesh for
726
920
  :type model_name: str
727
921
  """
728
- self.run_fiqus(data_model, model_name, "solve_only")
922
+ self.run_fiqus(data_model, model_name, run_type)
729
923
  self.model_name = model_name
730
924
 
731
925
  def compare_pos_files(
732
- self, pos_file_1: Union[str, os.PathLike], pos_file_2: Union[str, os.PathLike], rel_tolerance: float = 1e-10, abs_tolerance: float = 0
926
+ self, pos_file_1: Union[str, os.PathLike], pos_file_2: Union[str, os.PathLike],
927
+ rel_tolerance: float = 1e-10, abs_tolerance: float = 0.0, n_top_values_only: int = 0
733
928
  ):
734
929
  """
735
930
  This method compares the contents of two pos files. It is used to check that
@@ -739,6 +934,12 @@ class FiQuSSolverTests(BaseClassesForTests):
739
934
  :type pos_file_1: Union[str, os.PathLike]
740
935
  :param pos_file_2: path to the second pos file
741
936
  :type pos_file_2: Union[str, os.PathLike]
937
+ :param rel_tolerance: relative tolerance
938
+ :type rel_tolerance: float
939
+ :param abs_tolerance: absolute tolerance
940
+ :type abs_tolerance: float
941
+ :param n_top_values_only: if > 0 then the number of top n values is compared
942
+ :type n_top_values_only: int
742
943
  """
743
944
  # Initialize gmsh:
744
945
  gmsh_utils = GmshUtils(verbose=False)
@@ -753,7 +954,7 @@ class FiQuSSolverTests(BaseClassesForTests):
753
954
  # Open the pos file:
754
955
  gmsh.open(pos_file)
755
956
  data_all_steps = []
756
-
957
+
757
958
  while True:
758
959
  # Save all available time steps up to 100:
759
960
  try:
@@ -786,9 +987,19 @@ class FiQuSSolverTests(BaseClassesForTests):
786
987
  msg=f"{pos_file_1} and {pos_file_2} are not the same length!",
787
988
  )
788
989
 
789
- # Convert to numppy array:
790
- model_data1 = np.array(model_datas[0])
791
- model_data2 = np.array(model_datas[1])
990
+ # Convert to numpy arrays:
991
+ if n_top_values_only > 0:
992
+ print(f"Only top {n_top_values_only} values are compared.")
993
+ model_data1 = np.sort(np.partition(np.array(model_datas[0]), -n_top_values_only)[-n_top_values_only:])
994
+ model_data2 = np.sort(np.partition(np.array(model_datas[1]), -n_top_values_only)[-n_top_values_only:])
995
+ data = np.column_stack((model_data1, model_data2))
996
+
997
+ csv_path = f"{os.path.splitext(pos_file_1)[0]}.csv"
998
+ print(f'Saving {csv_path}')
999
+ # np.savetxt(csv_path, data, delimiter=",", header="pos_file_1,pos_file_2", comments='')
1000
+ else:
1001
+ model_data1 = np.array(model_datas[0])
1002
+ model_data2 = np.array(model_datas[1])
792
1003
 
793
1004
  # Compare the data:
794
1005
  np.testing.assert_allclose(
@@ -799,19 +1010,57 @@ class FiQuSSolverTests(BaseClassesForTests):
799
1010
  )
800
1011
 
801
1012
  def get_path_to_generated_file(
802
- self, data_model, file_name: str, file_extension: str
1013
+ self, data_model, model_name: str, file_extension: str, subfolder=None, run_type="solve_only"
803
1014
  ) -> Union[str, os.PathLike]:
804
1015
  return super().get_path_to_generated_file(
805
1016
  data_model,
806
1017
  self.model_name,
807
- file_name,
1018
+ model_name,
808
1019
  file_extension,
809
- run_type="solve_only",
1020
+ run_type=run_type,
1021
+ subfolder=subfolder
810
1022
  )
811
1023
 
812
1024
  def get_path_to_reference_file(
813
- self, data_model, file_name, file_extension
1025
+ self, data_model, model_name, file_extension, subfolder=None
814
1026
  ) -> Union[str, os.PathLike]:
815
1027
  return super().get_path_to_reference_file(
816
- data_model, self.model_name, file_name, file_extension, folder="Solution"
1028
+ data_model, self.model_name, model_name, file_extension, folder="Solution", subfolder=subfolder
1029
+ )
1030
+
1031
+
1032
+ class FiQuSPostProcessPythonTests(BaseClassesForTests):
1033
+ def post_process_python(
1034
+ self,
1035
+ data_model: FDM,
1036
+ model_name: str,
1037
+ ) -> Tuple[Union[str, os.PathLike], Union[str, os.PathLike]]:
1038
+ """
1039
+ This method postprocess with python the given model name.
1040
+
1041
+ :param data_model: data model to run FiQuS with
1042
+ :type data_model: FDM
1043
+ :param model_name: name of the model to generate the mesh for
1044
+ :type model_name: str
1045
+ """
1046
+ self.run_fiqus(data_model, model_name, "post_process_python_only")
1047
+ self.model_name = model_name
1048
+
1049
+ def get_path_to_generated_file(
1050
+ self, data_model, model_name: str, file_extension: str, subfolder=None
1051
+ ) -> Union[str, os.PathLike]:
1052
+ return super().get_path_to_generated_file(
1053
+ data_model,
1054
+ self.model_name,
1055
+ model_name,
1056
+ file_extension,
1057
+ subfolder=subfolder,
1058
+ run_type="post_process_python_only",
817
1059
  )
1060
+
1061
+ def get_path_to_reference_file(
1062
+ self, data_model, model_name, file_extension, subfolder=None
1063
+ ) -> Union[str, os.PathLike]:
1064
+ return super().get_path_to_reference_file(
1065
+ data_model, self.model_name, model_name, file_extension, folder="Solution", subfolder=subfolder
1066
+ )