Coverage for neuber_correction\performance_test.py: 0%

126 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-08-14 08:34 +0200

1""" 

2Performance tests for the NeuberCorrection class. 

3 

4This module provides comprehensive performance benchmarking for the Neuber correction 

5functionality, including timing comparisons with and without memoization. 

6""" 

7 

8import gc 

9import random 

10import sys 

11import time 

12 

13from neuber_correction import ( 

14 MaterialForNeuberCorrection, 

15 NeuberCorrection, 

16 NeuberSolverSettings, 

17) 

18 

19 

20def format_time(seconds: float) -> str: 

21 """Format time in seconds to a human-readable string.""" 

22 if seconds < 1e-6: 

23 return f"{seconds * 1e9:.2f} ns" 

24 if seconds < 1e-3: 

25 return f"{seconds * 1e6:.2f} μs" 

26 if seconds < 1: 

27 return f"{seconds * 1e3:.2f} ms" 

28 return f"{seconds:.3f} s" 

29 

30 

31def get_memory_usage(): 

32 """Get current memory usage information.""" 

33 

34 gc.collect() # Force garbage collection 

35 

36 # Count instances 

37 instance_count = len(NeuberCorrection.instances) 

38 total_cache_entries = sum( 

39 len(instance.memoization_table) 

40 for instance in NeuberCorrection.instances.values() 

41 ) 

42 

43 return { 

44 "instance_count": instance_count, 

45 "total_cache_entries": total_cache_entries, 

46 } 

47 

48 

49def benchmark_memoization_effectiveness(): 

50 """Benchmark the effectiveness of memoization with clear isolation.""" 

51 # Clear all existing instances to ensure clean state 

52 NeuberCorrection.clear_all_instances() 

53 

54 # Check initial memory state 

55 initial_memory = get_memory_usage() 

56 

57 print("NEUBER CORRECTION MEMOIZATION BENCHMARK") 

58 print("=" * 60) 

59 print( 

60 f"Initial state: {initial_memory['instance_count']} instances, " 

61 f"{initial_memory['total_cache_entries']} cache entries" 

62 ) 

63 print() 

64 

65 # Create test material (S355 steel) 

66 material = MaterialForNeuberCorrection( 

67 yield_strength=315, 

68 sigma_u=470, 

69 elastic_mod=210000, 

70 eps_u=0.12, 

71 ) 

72 

73 # Test stress values 

74 stress_values = [400, 500, 600, 700, 800, 900, 1000] 

75 

76 print("Material: S355 Steel") 

77 print(f"Test stresses: {stress_values}") 

78 print() 

79 

80 # Test 1: No memoization (very high precision) 

81 print("TEST 1: No Memoization (Precision = 1e-12)") 

82 print("-" * 40) 

83 

84 settings_no_memo = NeuberSolverSettings(memoization_precision=1e-12) 

85 neuber_no_memo = NeuberCorrection(material=material, settings=settings_no_memo) 

86 

87 # First run - all calculations 

88 start_time = time.perf_counter() 

89 for stress in stress_values: 

90 neuber_no_memo.correct_stress_values([stress]) 

91 first_run_time = time.perf_counter() - start_time 

92 

93 # Second run - should recalculate everything 

94 start_time = time.perf_counter() 

95 for stress in stress_values: 

96 neuber_no_memo.correct_stress_values([stress]) 

97 second_run_time = time.perf_counter() - start_time 

98 

99 print(f"First run: {format_time(first_run_time)}") 

100 print(f"Second run: {format_time(second_run_time)}") 

101 print(f"Cache entries: {len(neuber_no_memo.memoization_table)}") 

102 print() 

103 

104 # Test 2: With memoization (realistic precision) 

105 print("TEST 2: With Memoization (Precision = 0.1 MPa)") 

106 print("-" * 40) 

107 

108 settings_with_memo = NeuberSolverSettings(memoization_precision=0.1) 

109 neuber_with_memo = NeuberCorrection(material=material, settings=settings_with_memo) 

110 

111 # First run - all calculations 

112 start_time = time.perf_counter() 

113 for stress in stress_values: 

114 neuber_with_memo.correct_stress_values([stress]) 

115 first_run_time = time.perf_counter() - start_time 

116 

117 # Second run - should use cache 

118 start_time = time.perf_counter() 

119 for stress in stress_values: 

120 neuber_with_memo.correct_stress_values([stress]) 

121 second_run_time = time.perf_counter() - start_time 

122 

123 print(f"First run: {format_time(first_run_time)}") 

124 print(f"Second run: {format_time(second_run_time)}") 

125 print(f"Cache entries: {len(neuber_with_memo.memoization_table)}") 

126 print(f"Speedup: {first_run_time / second_run_time:.2f}x") 

127 print() 

128 

129 # Test 3: Precision-based cache hits 

130 print("TEST 3: Precision-Based Cache Hits") 

131 print("-" * 40) 

132 

133 settings_precision = NeuberSolverSettings( 

134 memoization_precision=1.0 

135 ) # 1 MPa precision 

136 neuber_precision = NeuberCorrection(material=material, settings=settings_precision) 

137 

138 # Calculate for stress 500 

139 start_time = time.perf_counter() 

140 result1 = neuber_precision.correct_stress_values([500])[0] 

141 time1 = time.perf_counter() - start_time 

142 

143 # Calculate for stress 500.5 (within 1 MPa precision) 

144 start_time = time.perf_counter() 

145 result2 = neuber_precision.correct_stress_values([500.5])[0] 

146 time2 = time.perf_counter() - start_time 

147 

148 # Calculate for stress 502 (outside 1 MPa precision) 

149 start_time = time.perf_counter() 

150 result3 = neuber_precision.correct_stress_values([502])[0] 

151 time3 = time.perf_counter() - start_time 

152 

153 print(f"Stress 500.0: {format_time(time1)} -> {result1:.2f} MPa") 

154 print( 

155 f"Stress 500.5: {format_time(time2)} -> {result2:.2f} MPa (cache hit: {result1 == result2})" 

156 ) 

157 print( 

158 f"Stress 502.0: {format_time(time3)} -> {result3:.2f} MPa (cache hit: {result1 == result3})" 

159 ) 

160 print(f"Cache entries: {len(neuber_precision.memoization_table)}") 

161 print() 

162 

163 # Test 4: Large dataset performance 

164 print("TEST 4: Large Dataset Performance") 

165 print("-" * 40) 

166 

167 # Generate 1000 stress values with many duplicates 

168 random.seed(42) # For reproducible results 

169 

170 # Create a smaller set of unique values 

171 unique_stresses = [500 + i * 10 for i in range(5000)] # 50 unique values 

172 

173 large_stress_list = [] 

174 for i in range(1000): 

175 # Pick from the unique values (many duplicates) 

176 large_stress_list.append(random.choice(unique_stresses)) 

177 

178 # Test without memoization (very high precision = no cache hits) 

179 settings_large_no_memo = NeuberSolverSettings(memoization_precision=1e-12) 

180 neuber_large_no_memo = NeuberCorrection( 

181 material=material, settings=settings_large_no_memo 

182 ) 

183 

184 start_time = time.perf_counter() 

185 results_no_memo = neuber_large_no_memo.correct_stress_values(large_stress_list) 

186 time_no_memo = time.perf_counter() - start_time 

187 

188 # Test with memoization (same precision, will benefit from duplicates) 

189 settings_large_with_memo = NeuberSolverSettings( 

190 memoization_precision=1e-12 

191 ) # Same precision 

192 neuber_large_with_memo = NeuberCorrection( 

193 material=material, settings=settings_large_with_memo 

194 ) 

195 

196 start_time = time.perf_counter() 

197 results_with_memo = neuber_large_with_memo.correct_stress_values(large_stress_list) 

198 time_with_memo = time.perf_counter() - start_time 

199 

200 print(f"Dataset size: {len(large_stress_list)} stress values") 

201 print(f"Without memoization: {format_time(time_no_memo)}") 

202 print(f"With memoization: {format_time(time_with_memo)}") 

203 print(f"Speedup: {time_no_memo / time_with_memo:.2f}x") 

204 print(f"Cache entries: {len(neuber_large_with_memo.memoization_table)}") 

205 print( 

206 f"Results identical: {all(abs(a - b) < 1e-10 for a, b in zip(results_no_memo, results_with_memo))}" 

207 ) 

208 print( 

209 "Max difference: ", 

210 max(abs(a - b) for a, b in zip(results_no_memo, results_with_memo)), 

211 ) 

212 

213 # Test 5: Memory efficiency 

214 print("TEST 5: Memory Efficiency") 

215 print("-" * 40) 

216 

217 # Test with different cache sizes 

218 cache_sizes = [100, 500, 1000, 2000] 

219 

220 for size in cache_sizes: 

221 settings_memory = NeuberSolverSettings(memoization_precision=0.1) 

222 neuber_memory = NeuberCorrection(material=material, settings=settings_memory) 

223 

224 # Generate stress values 

225 stress_values_memory = [200 + i * 0.1 for i in range(size)] 

226 

227 # Calculate all values 

228 for stress in stress_values_memory: 

229 neuber_memory.correct_stress_values([stress]) 

230 

231 cache_size = len(neuber_memory.memoization_table) 

232 memory_usage = sys.getsizeof(neuber_memory.memoization_table) 

233 

234 print( 

235 f"Cache size {size:4d}: {cache_size:4d} entries, {memory_usage:6d} bytes " 

236 f"({memory_usage/cache_size:.1f} bytes/entry)" 

237 ) 

238 

239 print() 

240 

241 # Check final memory state 

242 final_memory = get_memory_usage() 

243 print("=" * 60) 

244 print("BENCHMARK COMPLETE") 

245 print("=" * 60) 

246 print( 

247 f"Final state: {final_memory['instance_count']} instances, " 

248 f"{final_memory['total_cache_entries']} cache entries" 

249 ) 

250 print( 

251 f"Memory growth: {final_memory['instance_count'] - initial_memory['instance_count']} instances, " 

252 f"{final_memory['total_cache_entries'] - initial_memory['total_cache_entries']} cache entries" 

253 ) 

254 

255 

256if __name__ == "__main__": 

257 benchmark_memoization_effectiveness()