Coverage for gamdpy/runtime_actions/stress_saver.py: 69%
115 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1import numpy as np
2import numba
3import math
4from numba import cuda, config
6from .runtime_action import RuntimeAction
9class StressSaver(RuntimeAction):
10 """ Runtime action for saving stress tensor(a D x D matrix) during a timeblock
11 every `steps_between_output` time steps.
12 """
13 def __init__(self, steps_between_output:int = 16, compute_flags = None, verbose=False) -> None:
14 if type(steps_between_output) != int or steps_between_output < 0:
15 raise ValueError(f'steps_between_output ({steps_between_output}) should be non-negative integer.')
16 self.steps_between_output = steps_between_output
17 self.compute_flags = compute_flags
18 self.verbose = verbose
20 def get_compute_flags(self):
21 return self.compute_flags
24 def setup(self, configuration, num_timeblocks:int, steps_per_timeblock:int, output, verbose=False) -> None:
26 self.configuration = configuration
27 D = configuration.D
29 if type(num_timeblocks) != int or num_timeblocks < 0:
30 raise ValueError(f'num_timeblocks ({num_timeblocks}) should be non-negative integer.')
31 self.num_timeblocks = num_timeblocks
33 if type(steps_per_timeblock) != int or steps_per_timeblock < 0:
34 raise ValueError(f'steps_per_timeblock ({steps_per_timeblock}) should be non-negative integer.')
35 self.steps_per_timeblock = steps_per_timeblock
37 if self.steps_between_output >= steps_per_timeblock:
38 raise ValueError(f'scalar_output ({self.steps_between_output}) must be less than steps_per_timeblock ({steps_per_timeblock})')
40 # per block saving of stress tensor
41 if not configuration.compute_flags['stresses']:
42 raise RuntimeError('stresses not set in compute_flags')
44 self.stress_saves_per_block = self.steps_per_timeblock//self.steps_between_output
46 # Setup output
47 shape = (self.num_timeblocks, self.stress_saves_per_block, D, D)
48 if 'stress_saver' in output.keys():
49 del output['stress_saver']
50 grp = output.create_group('stress_saver')
51 output.create_dataset('stress_saver/stress_tensor', shape=shape,
52 chunks=(1, self.stress_saves_per_block, D, D), dtype=np.float32)
53 grp.attrs['steps_between_output'] = self.steps_between_output
55 flag = config.CUDA_LOW_OCCUPANCY_WARNINGS
56 config.CUDA_LOW_OCCUPANCY_WARNINGS = False
57 self.zero_kernel = self.make_zero_kernel_3()
58 config.CUDA_LOW_OCCUPANCY_WARNINGS = flag
62 def make_zero_kernel_3(self):
63 """ Returns a kernel that can zero an array with three axes """
64 def zero_kernel(array):
65 Nx, Ny, Nz = array.shape
66 #i, j = cuda.grid(2) # doing simple 1 thread kernel for now ...
67 for i in range(Nx):
68 for j in range(Ny):
69 for k in range(Nz):
70 array[i,j,k] = numba.float32(0.0)
72 zero_kernel = cuda.jit(zero_kernel)
73 return zero_kernel[1,1]
76 def get_params(self, configuration, compute_plan):
77 D = configuration.D
78 self.output_array = np.zeros((self.stress_saves_per_block, D, D), dtype=np.float32)
79 self.d_output_array = cuda.to_device(self.output_array)
80 self.params = (self.steps_between_output, self.d_output_array)
81 return self.params
83 def initialize_before_timeblock(self, timeblock: int, output_reference):
84 self.zero_kernel(self.d_output_array)
86 def update_at_end_of_timeblock(self, timeblock: int, output_reference):
87 volume = self.configuration.get_volume()
88 output_reference['stress_saver/stress_tensor'][timeblock, :] = self.d_output_array.copy_to_host() / volume
91 def get_prestep_kernel(self, configuration, compute_plan):
92 # Unpack parameters from configuration and compute_plan
93 D, num_part = configuration.D, configuration.N
94 pb, tp, gridsync = [compute_plan[key] for key in ['pb', 'tp', 'gridsync']]
95 num_blocks = (num_part - 1) // pb + 1
98 m_id = configuration.sid['m']
99 v_id = configuration.vectors.indices['v']
100 sx_id = configuration.vectors.indices['sx']
101 sy_id = configuration.vectors.indices['sy']
102 if D > 2:
103 sz_id = configuration.vectors.indices['sz']
104 if D > 3:
105 sw_id = configuration.vectors.indices['sw']
107 #volume_function = numba.njit(configuration.simbox.get_volume_function())
109 def kernel(grid, vectors, scalars, r_im, sim_box, step, runtime_action_params):
110 """
111 """
112 steps_between_output, output_array = runtime_action_params # Needs to be compatible with get_params above
113 if step%steps_between_output==0:
114 save_index = step//steps_between_output
115 if save_index < output_array.shape[0]:
116 global_id, my_t = cuda.grid(2)
117 if global_id < num_part and my_t == 0:
118 my_m = scalars[global_id][m_id]
119 for k in range(D):
120 cuda.atomic.add(output_array, (save_index, 0, k), vectors[sx_id][global_id][k] -
121 my_m * vectors[v_id][global_id][0]*vectors[v_id][global_id][k])
123 cuda.atomic.add(output_array, (save_index, 1, k), vectors[sy_id][global_id][k] -
124 my_m * vectors[v_id][global_id][1]*vectors[v_id][global_id][k])
125 if D > 2:
126 cuda.atomic.add(output_array, (save_index, 2, k), vectors[sz_id][global_id][k] -
127 my_m * vectors[v_id][global_id][2]*vectors[v_id][global_id][k])
128 if D > 3:
129 cuda.atomic.add(output_array, (save_index, 3, k), vectors[sw_id][global_id][k] -
130 my_m * vectors[v_id][global_id][3]*vectors[v_id][global_id][k])
131 return
133 kernel = cuda.jit(device=gridsync)(kernel)
135 if gridsync:
136 return kernel # return device function
137 else:
138 return kernel[num_blocks, (pb, 1)] # return kernel, incl. launch parameters
140 #### CONSIDER USING POSTSTEP TO GET CORRECT VELOCITIES ###############
141 def get_poststep_kernel(self, configuration, compute_plan, verbose=False):
142 pb, tp, gridsync = [compute_plan[key] for key in ['pb', 'tp', 'gridsync']]
143 if gridsync:
144 def kernel(grid, vectors, scalars, r_im, sim_box, step, conf_saver_params):
145 pass
146 return
147 return cuda.jit(device=gridsync)(kernel)
148 else:
149 def kernel(grid, vectors, scalars, r_im, sim_box, step, conf_saver_params):
150 pass
151 return kernel
153 # Class functions to read data
155 def extract(h5file, first_block=0, last_block=None, subsample=1):
157 #stress_data = h5file['stress_saver/stress_tensor']
158 h5grp = h5file['stress_saver']
159 nblocks, per_block, D, D2 = h5grp['stress_tensor'].shape
160 assert D == D2
161 final_rows = (nblocks-first_block) * per_block
162 return h5grp['stress_tensor'][first_block:last_block,:,:, :].reshape(final_rows, D, D)[::subsample]
164 def get_times(h5file, first_block=0, last_block=-1, reset_time=True, subsample=1):
165 num_timeblock, saves_per_timeblock = h5file['stress_saver']['stress_tensor'][first_block:last_block,:,0,0].shape
166 times_array = np.arange(0,num_timeblock*saves_per_timeblock, step=subsample)*h5file.attrs['dt']
167 return times_array
169def extract_stress_tensor(data, first_block=0, D=3):
170 """ Extracts stress tensor data from simulation output.
172 Parameters
173 ----------
175 data : dict
176 Output from a Simulation object.
180 first_block : int
181 Index of the first timeblock to extract data from.
183 D : int
184 Dimension of the simulation.
186 Returns
187 -------
189 array
190 three-dimensional numpy array containing the extracted tensor data.
192 """
193 stress_data = data['stress_saver/stress_tensor']
194 nblocks, per_block, _, _ = stress_data.shape
195 final_rows = (nblocks-first_block) * per_block
196 return stress_data[first_block:,:,:].reshape(final_rows, D, D)