ANYsolver 0.1.0__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 (70) hide show
  1. anysolver/__init__.py +631 -0
  2. anysolver/anystructure_fem_mode.py +942 -0
  3. anysolver/arc_length.py +758 -0
  4. anysolver/assembly.py +950 -0
  5. anysolver/baselines.py +303 -0
  6. anysolver/beam_shell_verification.py +4276 -0
  7. anysolver/beam_validity.py +110 -0
  8. anysolver/benchmarks.py +495 -0
  9. anysolver/boundary.py +476 -0
  10. anysolver/buckling.py +442 -0
  11. anysolver/buckling_validity.py +91 -0
  12. anysolver/capacity_workflow.py +350 -0
  13. anysolver/cases.py +173 -0
  14. anysolver/composite_strip_verification.py +297 -0
  15. anysolver/contact.py +3461 -0
  16. anysolver/corotational.py +371 -0
  17. anysolver/cylinder_benchmarks.py +364 -0
  18. anysolver/dynamics.py +723 -0
  19. anysolver/element_qualification.py +432 -0
  20. anysolver/elements.py +2724 -0
  21. anysolver/external_references.py +369 -0
  22. anysolver/fe_core.py +327 -0
  23. anysolver/fracture.py +551 -0
  24. anysolver/imperfections.py +390 -0
  25. anysolver/jit_compiler.py +108 -0
  26. anysolver/kernel_warmup.py +180 -0
  27. anysolver/linalg.py +760 -0
  28. anysolver/mass_properties.py +179 -0
  29. anysolver/material_curves.py +231 -0
  30. anysolver/matrix_assembly.py +558 -0
  31. anysolver/mesh_gen.py +1065 -0
  32. anysolver/mesh_load_bc_verification.py +609 -0
  33. anysolver/modal.py +282 -0
  34. anysolver/nonlinear.py +300 -0
  35. anysolver/nonlinear_performance.py +920 -0
  36. anysolver/nonlinear_performance_batch_b.py +592 -0
  37. anysolver/nonlinear_performance_batch_c.py +506 -0
  38. anysolver/nonlinear_performance_bootstrap.py +120 -0
  39. anysolver/nonlinear_reduced_assembly.py +760 -0
  40. anysolver/nonlinear_static.py +1518 -0
  41. anysolver/plasticity.py +419 -0
  42. anysolver/plasticity_qualification.py +314 -0
  43. anysolver/production_readiness.py +304 -0
  44. anysolver/quality_control.py +1497 -0
  45. anysolver/recovery.py +505 -0
  46. anysolver/recovery_policy.py +205 -0
  47. anysolver/reference_cases.py +425 -0
  48. anysolver/results.py +503 -0
  49. anysolver/runtime.py +9030 -0
  50. anysolver/s4_validity.py +326 -0
  51. anysolver/sesam_fem/__init__.py +55 -0
  52. anysolver/sesam_fem/__main__.py +80 -0
  53. anysolver/sesam_fem/diagnostics.py +65 -0
  54. anysolver/sesam_fem/document.py +814 -0
  55. anysolver/sesam_fem/exporter.py +84 -0
  56. anysolver/sesam_fem/importer.py +365 -0
  57. anysolver/sesam_fem/records.py +257 -0
  58. anysolver/sesam_fem/schema.py +107 -0
  59. anysolver/sesam_fem/sif_importer.py +397 -0
  60. anysolver/sesam_fem/validation.py +62 -0
  61. anysolver/shell_benchmarks.py +367 -0
  62. anysolver/test_cases.py +610 -0
  63. anysolver/validation.py +416 -0
  64. anysolver/vectorized_nonlinear.py +334 -0
  65. anysolver/vectorized_stiffness.py +571 -0
  66. anysolver-0.1.0.dist-info/METADATA +165 -0
  67. anysolver-0.1.0.dist-info/RECORD +70 -0
  68. anysolver-0.1.0.dist-info/WHEEL +5 -0
  69. anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
  70. anysolver-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,610 @@
1
+ """
2
+ Comprehensive Test Cases for FE Solver
3
+
4
+ This module provides realistic test cases for ship structural analysis,
5
+ including comparisons with semi-analytical solutions where available.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, List, Dict, Tuple, Optional, Any
11
+ import numpy as np
12
+ import json
13
+ import os
14
+ import time
15
+
16
+ if TYPE_CHECKING:
17
+ from .fe_core import FEModel
18
+ from .boundary import LoadCase
19
+ from .results import FEResult
20
+
21
+
22
+ @dataclass
23
+ class TestCase:
24
+ """Base class for test cases."""
25
+ name: str
26
+ description: str
27
+ category: str = "General"
28
+
29
+ def run(self) -> Dict[str, Any]:
30
+ """Run the test case."""
31
+ raise NotImplementedError
32
+
33
+ def to_dict(self) -> Dict[str, Any]:
34
+ return {
35
+ 'name': self.name,
36
+ 'description': self.description,
37
+ 'category': self.category
38
+ }
39
+
40
+
41
+ @dataclass
42
+ class ShipPanelTestCase(TestCase):
43
+ """
44
+ Test Case: Typical Ship Stiffened Panel
45
+
46
+ This test case models a typical ship panel with:
47
+ - Plate: 2000 x 1000 mm, 15 mm thick
48
+ - Stiffeners: T-bar, 100 mm web, 8 mm web thickness, 100x12 mm flange
49
+ - Spacing: 600 mm
50
+ - Material: Ship steel (E=210 GPa, ν=0.3, σy=235 MPa)
51
+ - Loading: Lateral pressure + in-plane loads
52
+
53
+ Includes simplified checks inspired by DNV-CG-0128 guidance.
54
+ """
55
+
56
+ # Geometry parameters (mm)
57
+ panel_length: float = 2000.0
58
+ panel_width: float = 1000.0
59
+ plate_thickness: float = 15.0
60
+
61
+ # Stiffener parameters (mm)
62
+ stiffener_type: str = "T-bar"
63
+ stiffener_spacing: float = 600.0
64
+ web_height: float = 100.0
65
+ web_thickness: float = 8.0
66
+ flange_width: float = 100.0
67
+ flange_thickness: float = 12.0
68
+
69
+ # Material properties
70
+ elastic_modulus: float = 210e9 # Pa
71
+ poisson_ratio: float = 0.3
72
+ yield_stress: float = 235e6 # Pa
73
+
74
+ # Loading
75
+ lateral_pressure: float = 0.05e6 # Pa (50 kPa)
76
+ axial_stress: float = 100e6 # Pa (100 MPa)
77
+ transverse_stress: float = 50e6 # Pa
78
+ shear_stress: float = 30e6 # Pa
79
+
80
+ # Boundary conditions
81
+ in_plane_support: str = "Integrated"
82
+ rotational_support: str = "SS"
83
+
84
+ # Mesh settings
85
+ shell_divisions_x: int = 8
86
+ shell_divisions_y: int = 4
87
+ beam_divisions: int = 4
88
+
89
+ def __init__(self):
90
+ super().__init__(
91
+ name="Ship Stiffened Panel",
92
+ description="Typical ship panel with T-bar stiffeners under combined loading",
93
+ category="Ship Structures"
94
+ )
95
+
96
+ def run(self) -> Dict[str, Any]:
97
+ """Run the ship panel test case."""
98
+ from . import (
99
+ PanelGeometry, MeshConfig, generate_stiffened_panel_mesh,
100
+ LoadCase, solve_linear, create_fe_result, FixedSupport
101
+ )
102
+
103
+ result = {
104
+ 'test_case': self.name,
105
+ 'geometry': self.to_dict(),
106
+ 'results': {},
107
+ 'comparisons': {}
108
+ }
109
+
110
+ # Create panel geometry
111
+ panel = PanelGeometry(
112
+ length=self.panel_length / 1000, # Convert to meters
113
+ width=self.panel_width / 1000,
114
+ plate_thickness=self.plate_thickness / 1000,
115
+ stiffener_type=self.stiffener_type,
116
+ stiffener_spacing=self.stiffener_spacing / 1000,
117
+ stiffener_height=self.web_height / 1000,
118
+ stiffener_web_thickness=self.web_thickness / 1000,
119
+ stiffener_flange_width=self.flange_width / 1000,
120
+ stiffener_flange_thickness=self.flange_thickness / 1000,
121
+ num_stiffeners=int(self.panel_width / self.stiffener_spacing),
122
+ in_plane_support=self.in_plane_support,
123
+ rotational_support=self.rotational_support,
124
+ axial_stress=self.axial_stress,
125
+ transverse_stress=self.transverse_stress,
126
+ shear_stress=self.shear_stress,
127
+ pressure=self.lateral_pressure
128
+ )
129
+
130
+ # Create mesh configuration
131
+ config = MeshConfig(
132
+ shell_num_divisions_x=self.shell_divisions_x,
133
+ shell_num_divisions_y=self.shell_divisions_y,
134
+ beam_num_divisions=self.beam_divisions,
135
+ use_coupling_elements=True,
136
+ coupling_stiffness=1e12
137
+ )
138
+
139
+ # Generate mesh
140
+ model = generate_stiffened_panel_mesh(panel, config)
141
+
142
+ result['results']['mesh'] = {
143
+ 'num_nodes': model.mesh.num_nodes,
144
+ 'num_elements': model.mesh.num_elements,
145
+ 'num_shell_elements': len([e for e in model.mesh.elements.values() if e.__class__.__name__ == 'ShellElement']),
146
+ 'num_beam_elements': len([e for e in model.mesh.elements.values() if e.__class__.__name__ == 'BeamElement']),
147
+ 'num_coupling_elements': len([e for e in model.mesh.elements.values() if e.__class__.__name__ == 'CoupledBeamShellElement'])
148
+ }
149
+
150
+ # Add edge fixity only to shell/master nodes. Beam nodes may be MPC
151
+ # slaves in the eccentric beam-shell coupling and must not be fixed.
152
+ edge_nodes = []
153
+ L = panel.length
154
+ W = panel.width
155
+ for node_id, node in model.mesh.nodes.items():
156
+ x, y, z = node.x, node.y, node.z
157
+ if (abs(z) < 1e-9 and
158
+ (abs(x) < 1e-6 or abs(x - L) < 1e-6 or
159
+ abs(y) < 1e-6 or abs(y - W) < 1e-6)):
160
+ edge_nodes.append(node_id)
161
+
162
+ for node_id in edge_nodes:
163
+ model.add_boundary_condition(FixedSupport(f"Edge_{node_id}", [node_id]))
164
+
165
+ # Create load case
166
+ load_case = LoadCase(name="combined_loading")
167
+
168
+ # Apply lateral pressure to all shell elements
169
+ for elem_id, element in model.mesh.elements.items():
170
+ if element.__class__.__name__ == 'ShellElement':
171
+ load_case.add_pressure_load(elem_id, pressure=self.lateral_pressure)
172
+
173
+ # Apply in-plane loads (simplified as nodal loads)
174
+ # This would be more accurate with proper in-plane load application
175
+
176
+ # Solve
177
+ start_time = time.time()
178
+ displacements, solver_info = solve_linear(model, load_case, solver_type='direct')
179
+ solve_time = time.time() - start_time
180
+
181
+ result['results']['solver'] = {
182
+ 'solve_time': solve_time,
183
+ 'convergence': solver_info.get('convergence_info', {}).get('status', 'unknown'),
184
+ 'num_free_dofs': solver_info.get('num_free_dofs', 0),
185
+ 'num_constrained_dofs': solver_info.get('num_constrained_dofs', 0)
186
+ }
187
+
188
+ # Create result object
189
+ solver_info_copy = solver_info.copy()
190
+ solver_info_copy['load_case'] = load_case
191
+ fe_result = create_fe_result(model, displacements, solver_info_copy)
192
+
193
+ # Extract key results
194
+ max_disp = fe_result.max_displacement
195
+ max_disp_node = fe_result.max_displacement_node
196
+
197
+ result['results']['displacements'] = {
198
+ 'max_displacement': max_disp,
199
+ 'max_displacement_node': max_disp_node,
200
+ 'displacement_norm': fe_result.get_displacement_norm()
201
+ }
202
+
203
+ # Calculate stresses (simplified)
204
+ # For a more complete analysis, we would compute stresses at integration points
205
+
206
+ result['results']['stresses'] = {
207
+ 'note': 'Stress calculation would be added in post-processing'
208
+ }
209
+
210
+ # Design checks
211
+ result['design_checks'] = self.perform_design_checks(fe_result, panel)
212
+
213
+ return result
214
+
215
+ def perform_design_checks(self, fe_result: 'FEResult', panel: 'PanelGeometry') -> Dict[str, Any]:
216
+ """Perform basic design checks."""
217
+ checks = {}
218
+
219
+ # Check 1: Displacement limit (typical: L/200 for serviceability)
220
+ L = panel.length
221
+ max_disp = fe_result.max_displacement
222
+ displacement_limit = L / 200
223
+ displacement_utilization = max_disp / displacement_limit if displacement_limit > 0 else 0
224
+
225
+ checks['displacement'] = {
226
+ 'max_displacement': max_disp,
227
+ 'limit': displacement_limit,
228
+ 'utilization': displacement_utilization,
229
+ 'status': 'OK' if displacement_utilization < 1.0 else 'EXCEEDS'
230
+ }
231
+
232
+ # Check 2: Stress limit (yield stress)
233
+ # Note: Actual stress calculation would be needed
234
+ checks['stress'] = {
235
+ 'yield_stress': self.yield_stress,
236
+ 'note': 'Stress check requires stress recovery from elements',
237
+ 'status': 'NOT_AVAILABLE'
238
+ }
239
+
240
+ # Check 3: Buckling check (simplified)
241
+ # Based on plate slenderness
242
+ plate_slenderness = (panel.width / panel.plate_thickness) * np.sqrt(
243
+ self.yield_stress / (210e9 * np.pi**2)
244
+ )
245
+ checks['buckling'] = {
246
+ 'plate_slenderness': plate_slenderness,
247
+ 'limit': 200.0, # Typical limit from DNV
248
+ 'status': 'OK' if plate_slenderness < 200.0 else 'EXCEEDS'
249
+ }
250
+
251
+ return checks
252
+
253
+
254
+ @dataclass
255
+ class BucklingTestCase(TestCase):
256
+ """
257
+ Test Case: Plate Buckling Under Compression
258
+
259
+ This test case verifies the solver's ability to capture buckling behavior
260
+ by comparing with analytical buckling coefficients.
261
+
262
+ Analytical solution for simply supported plate:
263
+ σ_cr = k * π² * E / (12 * (1-ν²)) * (t/b)²
264
+
265
+ Where k is the buckling coefficient (4.0 for simply supported square plate)
266
+ """
267
+
268
+ panel_length: float = 1000.0 # mm
269
+ panel_width: float = 1000.0 # mm
270
+ plate_thickness: float = 10.0 # mm
271
+
272
+ elastic_modulus: float = 210e9 # Pa
273
+ poisson_ratio: float = 0.3
274
+
275
+ # Buckling coefficient for simply supported square plate
276
+ buckling_coefficient: float = 4.0
277
+
278
+ def __init__(self):
279
+ super().__init__(
280
+ name="Plate Buckling",
281
+ description="Plate buckling under uniform compression",
282
+ category="Stability"
283
+ )
284
+
285
+ def run(self) -> Dict[str, Any]:
286
+ """Run buckling test case."""
287
+ from . import generate_simple_panel_mesh, LoadCase, solve_linear
288
+
289
+ result = {
290
+ 'test_case': self.name,
291
+ 'parameters': self.to_dict(),
292
+ 'analytical': {},
293
+ 'fe_results': {},
294
+ 'comparisons': {}
295
+ }
296
+
297
+ # Calculate analytical buckling stress
298
+ b = self.panel_width / 1000 # Convert to meters
299
+ t = self.plate_thickness / 1000
300
+
301
+ sigma_cr_analytical = (self.buckling_coefficient * np.pi**2 * self.elastic_modulus /
302
+ (12 * (1 - self.poisson_ratio**2))) * (t / b)**2
303
+
304
+ result['analytical'] = {
305
+ 'buckling_stress': sigma_cr_analytical,
306
+ 'buckling_load': sigma_cr_analytical * t * b * 1000 # For 1m length
307
+ }
308
+
309
+ # Create FE model
310
+ model = generate_simple_panel_mesh(
311
+ length=self.panel_length / 1000,
312
+ width=self.panel_width / 1000,
313
+ thickness=self.plate_thickness / 1000,
314
+ num_divisions_x=10,
315
+ num_divisions_y=10
316
+ )
317
+
318
+ # Apply compressive load (as in-plane load)
319
+ # For linear analysis, we can only apply a fraction of the buckling load
320
+ applied_stress = sigma_cr_analytical * 0.5 # 50% of buckling stress
321
+
322
+ load_case = LoadCase(name="compression")
323
+ # Apply compressive stress to all nodes on one edge
324
+ for node_id, node in model.mesh.nodes.items():
325
+ if abs(node.y) < 1e-6: # Bottom edge
326
+ # Apply compressive force
327
+ area = (self.plate_thickness / 1000) * (self.panel_length / 1000 / 10)
328
+ force = applied_stress * area
329
+ load_case.add_nodal_load(node_id, forces=np.array([-force, 0, 0]))
330
+
331
+ # Fix the other edge
332
+ from .boundary import FixedSupport
333
+ for node_id, node in model.mesh.nodes.items():
334
+ if abs(node.y - self.panel_width / 1000) < 1e-6: # Top edge
335
+ model.add_boundary_condition(FixedSupport(f"Fixed_{node_id}", [node_id]))
336
+
337
+ # Solve
338
+ try:
339
+ displacements, solver_info = solve_linear(model, load_case, solver_type='direct')
340
+ max_disp = float(np.max(np.abs(displacements)))
341
+
342
+ result['fe_results'] = {
343
+ 'max_displacement': max_disp,
344
+ 'applied_stress': applied_stress,
345
+ 'buckling_stress_analytical': sigma_cr_analytical,
346
+ 'stress_ratio': applied_stress / sigma_cr_analytical
347
+ }
348
+
349
+ # Note: Linear analysis cannot capture buckling directly
350
+ # This would require nonlinear analysis or eigenvalue analysis
351
+ result['comparisons'] = {
352
+ 'note': 'Linear analysis cannot capture buckling. Eigenvalue analysis needed.',
353
+ 'recommendation': 'Implement eigenvalue solver for buckling analysis'
354
+ }
355
+
356
+ except Exception as e:
357
+ result['fe_results']['error'] = str(e)
358
+
359
+ return result
360
+
361
+
362
+ @dataclass
363
+ class VibrationTestCase(TestCase):
364
+ """
365
+ Test Case: Natural Frequency Analysis
366
+
367
+ This test case demonstrates how to perform modal analysis
368
+ to find natural frequencies of a stiffened panel.
369
+
370
+ Note: This requires eigenvalue solver implementation.
371
+ """
372
+
373
+ panel_length: float = 2000.0 # mm
374
+ panel_width: float = 1000.0 # mm
375
+ plate_thickness: float = 15.0 # mm
376
+
377
+ stiffener_spacing: float = 600.0 # mm
378
+ web_height: float = 100.0 # mm
379
+ web_thickness: float = 8.0 # mm
380
+ flange_width: float = 100.0 # mm
381
+ flange_thickness: float = 12.0 # mm
382
+
383
+ elastic_modulus: float = 210e9 # Pa
384
+ poisson_ratio: float = 0.3
385
+ density: float = 7850.0 # kg/m^3
386
+
387
+ def __init__(self):
388
+ super().__init__(
389
+ name="Natural Frequency",
390
+ description="Modal analysis of stiffened panel",
391
+ category="Dynamics"
392
+ )
393
+
394
+ def run(self) -> Dict[str, Any]:
395
+ """Run vibration test case."""
396
+ from . import PanelGeometry, MeshConfig, generate_stiffened_panel_mesh
397
+
398
+ result = {
399
+ 'test_case': self.name,
400
+ 'parameters': self.to_dict(),
401
+ 'results': {},
402
+ 'notes': []
403
+ }
404
+
405
+ # Create panel
406
+ panel = PanelGeometry(
407
+ length=self.panel_length / 1000,
408
+ width=self.panel_width / 1000,
409
+ plate_thickness=self.plate_thickness / 1000,
410
+ stiffener_type="T-bar",
411
+ stiffener_spacing=self.stiffener_spacing / 1000,
412
+ stiffener_height=self.web_height / 1000,
413
+ stiffener_web_thickness=self.web_thickness / 1000,
414
+ stiffener_flange_width=self.flange_width / 1000,
415
+ stiffener_flange_thickness=self.flange_thickness / 1000,
416
+ num_stiffeners=1
417
+ )
418
+
419
+ config = MeshConfig(
420
+ shell_num_divisions_x=8,
421
+ shell_num_divisions_y=4,
422
+ beam_num_divisions=8,
423
+ use_coupling_elements=True
424
+ )
425
+
426
+ model = generate_stiffened_panel_mesh(panel, config)
427
+
428
+ result['results'] = {
429
+ 'mesh': {
430
+ 'num_nodes': model.mesh.num_nodes,
431
+ 'num_elements': model.mesh.num_elements
432
+ },
433
+ 'material': {
434
+ 'elastic_modulus': self.elastic_modulus,
435
+ 'density': self.density,
436
+ 'poisson_ratio': self.poisson_ratio
437
+ }
438
+ }
439
+
440
+ result['notes'].append("Eigenvalue solver for modal analysis not yet implemented")
441
+ result['notes'].append("Would solve: (K - ω²M)u = 0 for natural frequencies")
442
+ result['notes'].append("Expected first mode: ~10-20 Hz for typical ship panel")
443
+
444
+ return result
445
+
446
+
447
+ @dataclass
448
+ class ThermalLoadTestCase(TestCase):
449
+ """
450
+ Test Case: Thermal Loading
451
+
452
+ This test case demonstrates thermal load analysis.
453
+
454
+ Note: Thermal analysis requires additional implementation.
455
+ """
456
+
457
+ panel_length: float = 1000.0 # mm
458
+ panel_width: float = 1000.0 # mm
459
+ plate_thickness: float = 10.0 # mm
460
+
461
+ elastic_modulus: float = 210e9 # Pa
462
+ poisson_ratio: float = 0.3
463
+ thermal_expansion: float = 12e-6 # 1/°C for steel
464
+
465
+ temperature_change: float = 50.0 # °C
466
+
467
+ def __init__(self):
468
+ super().__init__(
469
+ name="Thermal Loading",
470
+ description="Thermal expansion analysis",
471
+ category="Thermal"
472
+ )
473
+
474
+ def run(self) -> Dict[str, Any]:
475
+ """Run thermal load test case."""
476
+ result = {
477
+ 'test_case': self.name,
478
+ 'parameters': self.to_dict(),
479
+ 'analytical': {},
480
+ 'notes': []
481
+ }
482
+
483
+ # Calculate analytical thermal stress
484
+ # For a constrained plate: σ = E * α * ΔT
485
+ sigma_thermal = self.elastic_modulus * self.thermal_expansion * self.temperature_change
486
+
487
+ result['analytical'] = {
488
+ 'thermal_stress': sigma_thermal,
489
+ 'thermal_strain': self.thermal_expansion * self.temperature_change
490
+ }
491
+
492
+ result['notes'].append("Thermal load implementation not yet complete")
493
+ result['notes'].append("Would require: thermal strain matrix, temperature field")
494
+ result['notes'].append(f"Expected thermal stress: {sigma_thermal/1e6:.1f} MPa")
495
+
496
+ return result
497
+
498
+
499
+ class TestCaseRunner:
500
+ """Runner for test cases."""
501
+
502
+ TEST_CASES = {
503
+ 'ship_panel': ShipPanelTestCase,
504
+ 'buckling': BucklingTestCase,
505
+ 'vibration': VibrationTestCase,
506
+ 'thermal': ThermalLoadTestCase
507
+ }
508
+
509
+ @classmethod
510
+ def run_test_case(cls, test_case_name: str) -> Dict[str, Any]:
511
+ """Run a specific test case."""
512
+ if test_case_name not in cls.TEST_CASES:
513
+ raise ValueError(f"Unknown test case: {test_case_name}")
514
+
515
+ test_case = cls.TEST_CASES[test_case_name]()
516
+ return test_case.run()
517
+
518
+ @classmethod
519
+ def run_all_test_cases(cls) -> Dict[str, Dict[str, Any]]:
520
+ """Run all test cases."""
521
+ results = {}
522
+
523
+ for name, test_class in cls.TEST_CASES.items():
524
+ try:
525
+ test_case = test_class()
526
+ results[name] = test_case.run()
527
+ except Exception as e:
528
+ results[name] = {
529
+ 'test_case': name,
530
+ 'error': str(e),
531
+ 'status': 'FAILED'
532
+ }
533
+
534
+ return results
535
+
536
+ @classmethod
537
+ def save_test_case_results(cls, results: Dict[str, Dict[str, Any]], filename: str = "test_case_results.json"):
538
+ """Save test case results to file."""
539
+ import json
540
+
541
+ def convert_numpy_types(obj):
542
+ """Convert numpy types to native Python types for JSON serialization."""
543
+ if isinstance(obj, dict):
544
+ return {k: convert_numpy_types(v) for k, v in obj.items()}
545
+ elif isinstance(obj, list):
546
+ return [convert_numpy_types(item) for item in obj]
547
+ elif hasattr(obj, 'item'): # numpy scalar types (int64, float64, etc.)
548
+ return obj.item()
549
+ elif hasattr(obj, 'tolist'): # numpy arrays
550
+ return obj.tolist()
551
+ return obj
552
+
553
+ with open(filename, 'w') as f:
554
+ json.dump(convert_numpy_types(results), f, indent=2)
555
+
556
+ @classmethod
557
+ def print_test_case_summary(cls, results: Dict[str, Dict[str, Any]]):
558
+ """Print summary of test case results."""
559
+ print("=" * 70)
560
+ print("TEST CASE RESULTS SUMMARY")
561
+ print("=" * 70)
562
+
563
+ for name, result in results.items():
564
+ status = result.get('status', 'COMPLETED')
565
+ if 'error' in result:
566
+ status = 'FAILED'
567
+
568
+ print(f"\n{name.upper()}")
569
+ print("-" * len(name))
570
+ print(f"Status: {status}")
571
+
572
+ if 'error' in result:
573
+ print(f"Error: {result['error']}")
574
+ elif 'results' in result:
575
+ if 'max_displacement' in result['results']:
576
+ print(f"Max Displacement: {result['results']['max_displacement']:.6e} m")
577
+ if 'mesh' in result['results']:
578
+ print(f"Mesh: {result['results']['mesh']['num_nodes']} nodes, {result['results']['mesh']['num_elements']} elements")
579
+
580
+ print("\n" + "=" * 70)
581
+
582
+
583
+ def run_ship_panel_test() -> Dict[str, Any]:
584
+ """Run the ship panel test case."""
585
+ test_case = ShipPanelTestCase()
586
+ return test_case.run()
587
+
588
+
589
+ def run_all_demo_test_cases() -> Dict[str, Dict[str, Any]]:
590
+ """Run all demonstration test cases."""
591
+ return TestCaseRunner.run_all_test_cases()
592
+
593
+
594
+ if __name__ == "__main__":
595
+ import time
596
+
597
+ print("Running Comprehensive Test Cases...")
598
+ print("=" * 70)
599
+
600
+ start_time = time.time()
601
+ results = run_all_demo_test_cases()
602
+ elapsed_time = time.time() - start_time
603
+
604
+ TestCaseRunner.print_test_case_summary(results)
605
+
606
+ print(f"\nTotal time: {elapsed_time:.2f} seconds")
607
+
608
+ # Save results
609
+ TestCaseRunner.save_test_case_results(results, "test_case_results.json")
610
+ print("Results saved to test_case_results.json")