Skip to content

Commit ebc8dfa

Browse files
committed
adding benchmark test comparing accuracy, memory, convergence, runtime to capytaine
1 parent 230a8d5 commit ebc8dfa

1 file changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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

Comments
 (0)