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.
- fiqus/MainFiQuS.py +1 -8
- fiqus/data/DataConductor.py +4 -8
- fiqus/data/DataFiQuSMultipole.py +358 -167
- fiqus/data/DataModelCommon.py +30 -15
- fiqus/data/DataMultipole.py +33 -10
- fiqus/data/DataWindingsCCT.py +37 -37
- fiqus/data/RegionsModelFiQuS.py +1 -1
- fiqus/geom_generators/GeometryMultipole.py +751 -54
- fiqus/getdp_runners/RunGetdpMultipole.py +181 -31
- fiqus/mains/MainMultipole.py +109 -17
- fiqus/mesh_generators/MeshCCT.py +209 -209
- fiqus/mesh_generators/MeshMultipole.py +938 -263
- fiqus/parsers/ParserCOND.py +2 -1
- fiqus/parsers/ParserDAT.py +16 -16
- fiqus/parsers/ParserGetDPOnSection.py +212 -212
- fiqus/parsers/ParserGetDPTimeTable.py +134 -134
- fiqus/parsers/ParserMSH.py +53 -53
- fiqus/parsers/ParserRES.py +142 -142
- fiqus/plotters/PlotPythonCCT.py +133 -133
- fiqus/plotters/PlotPythonMultipole.py +18 -18
- fiqus/post_processors/PostProcessMultipole.py +16 -6
- fiqus/pre_processors/PreProcessCCT.py +175 -175
- fiqus/pro_assemblers/ProAssembler.py +3 -3
- fiqus/pro_material_functions/ironBHcurves.pro +246 -246
- fiqus/pro_templates/combined/CC_Module.pro +1213 -0
- fiqus/pro_templates/combined/ConductorAC_template.pro +1025 -0
- fiqus/pro_templates/combined/Multipole_template.pro +2738 -1338
- fiqus/pro_templates/combined/TSA_materials.pro +102 -2
- fiqus/pro_templates/combined/materials.pro +54 -3
- fiqus/utils/Utils.py +18 -25
- fiqus/utils/update_data_settings.py +1 -1
- {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/METADATA +64 -68
- {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/RECORD +42 -40
- {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/WHEEL +1 -1
- tests/test_geometry_generators.py +29 -32
- tests/test_mesh_generators.py +35 -34
- tests/test_solvers.py +32 -31
- tests/utils/fiqus_test_classes.py +396 -147
- tests/utils/generate_reference_files_ConductorAC.py +57 -57
- tests/utils/helpers.py +76 -1
- {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/LICENSE.txt +0 -0
- {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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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"
|
|
133
|
-
pass
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
166
|
+
shutil.copytree(
|
|
167
|
+
reference_mesh_folder, output_mesh_folder, dirs_exist_ok=True,
|
|
168
|
+
ignore=shutil.ignore_patterns('Solution*')
|
|
145
169
|
)
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
176
|
+
reference_mesh_folder, output_mesh_folder, dirs_exist_ok=True,
|
|
177
|
+
ignore=shutil.ignore_patterns('Solution*')
|
|
152
178
|
)
|
|
153
179
|
shutil.copytree(
|
|
154
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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,
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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: {
|
|
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):
|
|
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):
|
|
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(
|
|
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(
|
|
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]
|
|
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
|
-
|
|
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
|
|
554
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
self.
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1018
|
+
model_name,
|
|
808
1019
|
file_extension,
|
|
809
|
-
run_type=
|
|
1020
|
+
run_type=run_type,
|
|
1021
|
+
subfolder=subfolder
|
|
810
1022
|
)
|
|
811
1023
|
|
|
812
1024
|
def get_path_to_reference_file(
|
|
813
|
-
|
|
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,
|
|
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
|
+
)
|