1+ import os
2+ import sys
3+ import time
4+ import tracemalloc
5+ import numpy as np
6+
7+ # You must have Capytaine installed in the environment where you run this script:
8+ # pip install capytaine
9+ import capytaine as cpt
10+
11+ # --- Path Setup for OpenFLASH ---
12+ current_dir = os .path .dirname (os .path .abspath (__file__ ))
13+ src_dir = os .path .abspath (os .path .join (current_dir , '..' , 'src' ))
14+ if src_dir not in sys .path :
15+ sys .path .insert (0 , src_dir )
16+
17+ from openflash .meem_engine import MEEMEngine
18+ from openflash .meem_problem import MEEMProblem
19+ from openflash .basic_region_geometry import BasicRegionGeometry
20+ from openflash .geometry import ConcentricBodyGroup
21+ from openflash .body import SteppedBody
22+ from openflash .multi_equations import omega
23+ from openflash .multi_constants import g , rho
24+
25+ # ==========================================
26+ # Capytaine Helper Functions
27+ # ==========================================
28+ def get_points (a , d ):
29+ d_prime = d + [0 ]
30+ d_index = 0
31+ a_index = 0
32+ pt_lst = [(0 , - d [0 ])]
33+ for i in range (len (a )):
34+ pt_lst .append ((a [a_index ], - d_prime [d_index ]))
35+ d_index += 1
36+ pt_lst .append ((a [a_index ], - d_prime [d_index ]))
37+ a_index += 1
38+ return pt_lst
39+
40+ def get_f_densities (pt_lst , total_units ):
41+ face_lengths = np .array ([])
42+ for i in range (len (pt_lst ) - 1 ):
43+ p1 , p2 = pt_lst [i ], pt_lst [i + 1 ]
44+ face_length = abs (p2 [0 ] - p1 [0 ]) + abs (p2 [1 ] - p1 [1 ])
45+ face_lengths = np .append (face_lengths , face_length )
46+ total_length = sum (face_lengths )
47+ each_face_densities = np .vectorize (lambda x : max (1 , x / total_length * total_units ))(face_lengths )
48+ remainders = each_face_densities % 1
49+ each_face_densities = each_face_densities .astype (int )
50+ remaining_units = total_units - sum (each_face_densities )
51+ if remaining_units < 0 :
52+ for u in range (remaining_units * - 1 ):
53+ i = np .argmax (each_face_densities )
54+ each_face_densities [i ] = (each_face_densities [i ]) - 1
55+ else :
56+ for u in range (remaining_units ):
57+ i = np .argmax (remainders )
58+ each_face_densities [i ] = (each_face_densities [i ]) + 1
59+ remainders [i ] = 0
60+ assert sum (each_face_densities ) == total_units
61+ return each_face_densities
62+
63+ def make_face (p1 , p2 , f_density , t_density ):
64+ zarr = np .linspace (p1 [1 ], p2 [1 ], f_density + 1 )
65+ rarr = np .linspace (p1 [0 ], p2 [0 ], f_density + 1 )
66+ xyz = np .array ([np .array ([x / np .sqrt (2 ),y / np .sqrt (2 ),z ]) for x ,y ,z in zip (rarr ,rarr ,zarr )])
67+ return cpt .AxialSymmetricMesh .from_profile (xyz , nphi = t_density )
68+
69+ def faces_and_heaves (heaving , region , p1 , p2 , f_density , t_density , meshes , mask , panel_ct ):
70+ mesh = make_face (p1 , p2 , f_density , t_density )
71+ meshes += mesh
72+ new_panels = f_density * t_density
73+ if heaving [region ]:
74+ direction = [0 , 0 , 1 ]
75+ else :
76+ direction = [0 , 0 , 0 ]
77+ for i in range (new_panels ):
78+ mask .append (direction )
79+ return meshes , mask , (panel_ct + new_panels )
80+
81+ def make_body (pts , t_densities , f_densities , heaving ):
82+ meshes = cpt .meshes .meshes .Mesh ()
83+ panel_ct = 0
84+ mask = []
85+ for i in range ((len (pts ) - 1 ) // 2 ):
86+ p1 , p2 , p3 = pts [2 * i ], pts [2 * i + 1 ], pts [2 * i + 2 ]
87+ meshes , mask , panel_ct = faces_and_heaves (heaving , i , p1 , p2 , f_densities [2 * i ], t_densities [i ], meshes , mask , panel_ct )
88+ if p2 [1 ] < p3 [1 ]:
89+ region = i
90+ else :
91+ region = i + 1
92+ meshes , mask , panel_ct = faces_and_heaves (heaving , region , p2 , p3 , f_densities [2 * i + 1 ], t_densities [region ], meshes , mask , panel_ct )
93+
94+ # Suppress verbose mesh outputs
95+ real_stdout = sys .stdout
96+ sys .stdout = open (os .devnull , "w" )
97+ body = cpt .FloatingBody (meshes )
98+ sys .stdout = real_stdout
99+
100+ return body , panel_ct , mask
101+
102+ # ==========================================
103+ # Main Benchmark Logic
104+ # ==========================================
105+ def run_benchmark ():
106+ # 1. Shared physical configuration (Using `config5` logic)
107+ h = 1.001
108+ d = [0.5 , 0.25 ]
109+ a = [0.5 , 1.0 ]
110+ heaving = [1 , 0 ] # Inner cylinder heaves, outer doesn't
111+ t_densities = [50 , 100 ] # BEM angular panels
112+ face_units = 90 # BEM outline panels
113+ m0 = 1.0 # Wavenumber
114+ NMK = [50 , 50 , 50 ] # MEEM harmonics per region
115+
116+ print ("Running Capytaine BEM Solver..." )
117+ # Track Capytaine Memory & Runtime
118+ tracemalloc .start ()
119+ t0_cpt = time .perf_counter ()
120+
121+ pt_lst = get_points (a , d )
122+ f_densities = get_f_densities (pt_lst , face_units )
123+ body , panel_count , mask = make_body (pt_lst , t_densities , f_densities , heaving )
124+ body .dofs ["Heave" ] = mask
125+
126+ rad_problem = cpt .RadiationProblem (body = body , wavenumber = m0 , water_depth = h , rho = rho )
127+ solver = cpt .BEMSolver ()
128+ result_cpt = solver .solve (rad_problem )
129+
130+ t1_cpt = time .perf_counter ()
131+ _ , peak_cpt = tracemalloc .get_traced_memory ()
132+ tracemalloc .stop ()
133+
134+ cpt_time = t1_cpt - t0_cpt
135+ # Capytaine returns a flat dictionary for a single DOF
136+ cpt_am = float (result_cpt .added_mass ["Heave" ])
137+ cpt_damp = float (result_cpt .radiation_damping ["Heave" ])
138+
139+ print ("Running OpenFLASH MEEM Solver..." )
140+ # Track OpenFLASH Memory & Runtime
141+ tracemalloc .start ()
142+ t0_of = time .perf_counter ()
143+
144+ bodies = []
145+ for i in range (len (a )):
146+ bodies .append (SteppedBody (
147+ a = np .array ([a [i ]]),
148+ d = np .array ([d [i ]]),
149+ slant_angle = np .array ([0.0 ]),
150+ heaving = bool (heaving [i ])
151+ ))
152+
153+ arrangement = ConcentricBodyGroup (bodies )
154+ geometry = BasicRegionGeometry (arrangement , h = h , NMK = NMK )
155+ problem = MEEMProblem (geometry )
156+ problem .set_frequencies (np .array ([omega (m0 , h , g )]))
157+
158+ engine = MEEMEngine (problem_list = [problem ])
159+ X = engine .solve_linear_system_multi (problem , m0 )
160+ hydro_coeffs = engine .compute_hydrodynamic_coefficients (problem , X , m0 )
161+
162+ # Active mode correlates to the array index where heaving == 1
163+ active_idx = heaving .index (1 )
164+ of_am = float (hydro_coeffs [active_idx ]['real' ])
165+ of_damp = float (hydro_coeffs [active_idx ]['imag' ])
166+
167+ t1_of = time .perf_counter ()
168+ _ , peak_of = tracemalloc .get_traced_memory ()
169+ tracemalloc .stop ()
170+
171+ of_time = t1_of - t0_of
172+
173+ # ==========================================
174+ # Compute the requested metrics
175+ # ==========================================
176+ # Accuracy: Max difference in added mass and damping vs. Capytaine
177+ am_error = abs (cpt_am - of_am ) / abs (cpt_am )
178+ damp_error = abs (cpt_damp - of_damp ) / abs (cpt_damp )
179+ accuracy = (1.0 - max (am_error , damp_error )) * 100.0
180+
181+ # Runtime & Memory (as multipliers)
182+ runtime_speedup = cpt_time / of_time if of_time > 0 else float ('inf' )
183+ memory_reduction = peak_cpt / peak_of if peak_of > 0 else float ('inf' )
184+
185+ # Convergence: Defined here by Degrees of Freedom (Capytaine Panels / MEEM Matrix Size)
186+ of_matrix_size = NMK [0 ] + NMK [- 1 ] + 2 * sum (NMK [1 :- 1 ])
187+ convergence_factor = panel_count / of_matrix_size
188+
189+ print ("\n " + "=" * 50 )
190+ print (" BENCHMARK RESULTS" )
191+ print ("=" * 50 )
192+ print (f"Capytaine -> Time: { cpt_time :.4f} s | RAM: { peak_cpt / 1024 / 1024 :.2f} MB | AM: { cpt_am :.4f} | Damp: { cpt_damp :.4f} | DOFs: { panel_count } " )
193+ print (f"OpenFLASH -> Time: { of_time :.4f} s | RAM: { peak_of / 1024 / 1024 :.2f} MB | AM: { of_am :.4f} | Damp: { of_damp :.4f} | DOFs: { of_matrix_size } " )
194+ print ("=" * 50 + "\n " )
195+
196+ # Final output formatted explicitly to your prompt
197+ print (f"Comparisons to the Capytaine boundary element method software show that "
198+ f"OpenFLASH has { accuracy :.2f} % accuracy, "
199+ f"{ runtime_speedup :.2f} times faster runtime, "
200+ f"{ convergence_factor :.2f} x better convergence, "
201+ f"and { memory_reduction :.2f} times less memory use." )
202+
203+ if __name__ == '__main__' :
204+ run_benchmark ()
0 commit comments