PySDM_examples.utils.kinematic_2d.simulation
1import numpy as np 2 3from PySDM_examples.utils.kinematic_2d.make_default_product_collection import ( 4 make_default_product_collection, 5) 6from PySDM_examples.utils.kinematic_2d.mpdata_2d import MPDATA_2D 7from PySDM_examples.utils import DummyController 8from PySDM import Particulator 9from PySDM.backends import CPU 10from PySDM.dynamics import ( 11 AmbientThermodynamics, 12 Coalescence, 13 Collision, 14 Condensation, 15 Displacement, 16 EulerianAdvection, 17 Freezing, 18) 19from PySDM.environments import Kinematic2D 20from PySDM.initialisation.sampling import spatial_sampling 21 22 23class Simulation: 24 def __init__(self, settings, storage, SpinUp, backend=None): 25 self.settings = settings 26 self.storage = storage 27 self.particulator = None 28 self.backend = backend or CPU(settings.formulae, n_dims=2) 29 self.SpinUp = SpinUp 30 31 @property 32 def products(self): 33 return self.particulator.products 34 35 def reinit(self, products=None): 36 formulae = self.settings.formulae 37 backend = self.backend 38 initial_profiles = { 39 "th": self.settings.initial_dry_potential_temperature_profile, 40 "water_vapour_mixing_ratio": self.settings.initial_vapour_mixing_ratio_profile, 41 } 42 advectees = dict( 43 ( 44 key, 45 np.repeat( 46 profile.reshape(1, -1), 47 self.settings.grid[0], 48 axis=0, 49 ), 50 ) 51 for key, profile in initial_profiles.items() 52 ) 53 mpdata = MPDATA_2D( 54 advectees=advectees, 55 stream_function=self.settings.stream_function, 56 rhod_of_zZ=self.settings.rhod_of_zZ, 57 dt=self.settings.dt, 58 grid=self.settings.grid, 59 size=self.settings.size, 60 n_iters=self.settings.mpdata_iters, 61 infinite_gauge=self.settings.mpdata_iga, 62 nonoscillatory=self.settings.mpdata_fct, 63 third_order_terms=self.settings.mpdata_tot, 64 ) 65 environment = Kinematic2D( 66 dt=self.settings.dt, 67 grid=self.settings.grid, 68 size=self.settings.size, 69 rhod_of=self.settings.rhod_of_zZ, 70 mixed_phase=self.settings.processes["freezing"], 71 backend=backend, 72 solvers=mpdata, 73 ) 74 75 dynamics = [] 76 77 if products is not None: 78 products = list(products) 79 else: 80 products = make_default_product_collection(self.settings) 81 82 if self.settings.processes["fluid advection"]: 83 dynamics.append(AmbientThermodynamics()) 84 if self.settings.processes["condensation"]: 85 kwargs = {} 86 if not self.settings.condensation_adaptive: 87 kwargs["substeps"] = (self.settings.condensation_substeps,) 88 condensation = Condensation( 89 rtol_x=self.settings.condensation_rtol_x, 90 rtol_thd=self.settings.condensation_rtol_thd, 91 adaptive=self.settings.condensation_adaptive, 92 dt_cond_range=self.settings.condensation_dt_cond_range, 93 schedule=self.settings.condensation_schedule, 94 **kwargs, 95 ) 96 dynamics.append(condensation) 97 if self.settings.processes["fluid advection"]: 98 dynamics.append(EulerianAdvection()) 99 if self.settings.processes["particle advection"]: 100 dynamics.append( 101 Displacement( 102 enable_sedimentation=self.settings.processes["sedimentation"], 103 adaptive=self.settings.displacement_adaptive, 104 rtol=self.settings.displacement_rtol, 105 ) 106 ) 107 if ( 108 self.settings.processes["coalescence"] 109 and self.settings.processes["breakup"] 110 ): 111 dynamics.append( 112 Collision( 113 collision_kernel=self.settings.kernel, 114 enable_breakup=self.settings.processes["breakup"], 115 coalescence_efficiency=self.settings.coalescence_efficiency, 116 breakup_efficiency=self.settings.breakup_efficiency, 117 fragmentation_function=self.settings.breakup_fragmentation, 118 adaptive=self.settings.coalescence_adaptive, 119 dt_coal_range=self.settings.coalescence_dt_coal_range, 120 substeps=self.settings.coalescence_substeps, 121 optimized_random=self.settings.coalescence_optimized_random, 122 ) 123 ) 124 elif ( 125 self.settings.processes["coalescence"] 126 and not self.settings.processes["breakup"] 127 ): 128 dynamics.append( 129 Coalescence( 130 collision_kernel=self.settings.kernel, 131 adaptive=self.settings.coalescence_adaptive, 132 dt_coal_range=self.settings.coalescence_dt_coal_range, 133 substeps=self.settings.coalescence_substeps, 134 optimized_random=self.settings.coalescence_optimized_random, 135 ) 136 ) 137 assert not ( 138 self.settings.processes["breakup"] 139 and not self.settings.processes["coalescence"] 140 ) 141 if self.settings.processes["freezing"]: 142 dynamics.append( 143 Freezing( 144 immersion_freezing=self.settings.freezing_immersion, 145 thaw=self.settings.freezing_thaw, 146 ) 147 ) 148 149 attributes = environment.init_attributes( 150 spatial_discretisation=spatial_sampling.Pseudorandom(), 151 dry_radius_spectrum=self.settings.spectrum_per_mass_of_dry_air, 152 kappa=self.settings.kappa, 153 n_sd=self.settings.n_sd 154 // (2 if self.settings.freezing_inp_frac != 1 else 1), 155 ) 156 157 if self.settings.processes["freezing"]: 158 attributes["signed water mass"] = attributes.pop("water mass") 159 160 if self.settings.freezing_inp_spec is None: 161 immersed_surface_area = formulae.trivia.sphere_surface( 162 diameter=2 * formulae.trivia.radius(volume=attributes["dry volume"]) 163 ) 164 else: 165 immersed_surface_area = self.settings.freezing_inp_spec.percentiles( 166 np.random.random(attributes["dry volume"].size), # TODO #599: seed 167 ) 168 169 if self.settings.freezing_immersion == "singular": 170 attributes["freezing temperature"] = ( 171 formulae.freezing_temperature_spectrum.invcdf( 172 np.random.random(immersed_surface_area.size), # TODO #599: seed 173 immersed_surface_area, 174 ) 175 ) 176 else: 177 attributes["immersed surface area"] = immersed_surface_area 178 179 if self.settings.freezing_inp_frac != 1: 180 assert self.settings.n_sd % 2 == 0 181 assert 0 < self.settings.freezing_inp_frac < 1 182 freezing_attribute = { 183 "singular": "freezing temperature", 184 "time-dependent": "immersed surface area", 185 }[self.settings.freezing_immersion] 186 for name, array in attributes.items(): 187 if array.shape[-1] != self.settings.n_sd // 2: 188 raise AssertionError(f"attribute >>{name}<< has wrong size") 189 array = array.copy() 190 full_shape = list(array.shape) 191 orig = slice(None, full_shape[-1]) 192 copy = slice(orig.stop, None) 193 full_shape[-1] *= 2 194 attributes[name] = np.empty(full_shape, dtype=array.dtype) 195 if name == freezing_attribute: 196 attributes[name][orig] = array 197 attributes[name][copy] = 0 198 elif name == "multiplicity": 199 attributes[name][orig] = array * self.settings.freezing_inp_frac 200 attributes[name][copy] = array * ( 201 1 - self.settings.freezing_inp_frac 202 ) 203 elif len(array.shape) > 1: # particle positions 204 # TODO #599: seed 205 for dim, _ in enumerate(array.shape): 206 # only to make particles not shadow each other in visualisations 207 attributes[name][dim, orig] = array[dim, :] 208 attributes[name][dim, copy] = np.random.permutation( 209 array[dim, :] 210 ) 211 else: 212 attributes[name][orig] = array 213 attributes[name][copy] = array 214 215 non_zero_per_gridbox = np.count_nonzero( 216 attributes[freezing_attribute] 217 ) / np.prod(self.settings.grid) 218 assert non_zero_per_gridbox == self.settings.n_sd_per_gridbox / 2 219 220 self.particulator = Particulator( 221 n_sd=self.settings.n_sd, 222 environment=environment, 223 dynamics=dynamics, 224 attributes=attributes, 225 products=products, 226 ) 227 228 if self.SpinUp is not None: 229 self.SpinUp(self.particulator, self.settings.n_spin_up) 230 if self.storage is not None: 231 self.storage.init(self.settings) 232 233 def run(self, controller=DummyController(), vtk_exporter=None): 234 with controller: 235 for step in self.settings.output_steps: 236 if controller.panic: 237 break 238 239 self.particulator.advance(step - self.particulator.n_steps) 240 241 self.store(step) 242 243 if vtk_exporter is not None: 244 vtk_exporter.export_attributes(self.particulator) 245 vtk_exporter.export_products(self.particulator) 246 247 controller.set_percent(step / self.settings.output_steps[-1]) 248 249 def store(self, step): 250 for name, product in self.particulator.products.items(): 251 self.storage.save(product.get(), step, name)
class
Simulation:
24class Simulation: 25 def __init__(self, settings, storage, SpinUp, backend=None): 26 self.settings = settings 27 self.storage = storage 28 self.particulator = None 29 self.backend = backend or CPU(settings.formulae, n_dims=2) 30 self.SpinUp = SpinUp 31 32 @property 33 def products(self): 34 return self.particulator.products 35 36 def reinit(self, products=None): 37 formulae = self.settings.formulae 38 backend = self.backend 39 initial_profiles = { 40 "th": self.settings.initial_dry_potential_temperature_profile, 41 "water_vapour_mixing_ratio": self.settings.initial_vapour_mixing_ratio_profile, 42 } 43 advectees = dict( 44 ( 45 key, 46 np.repeat( 47 profile.reshape(1, -1), 48 self.settings.grid[0], 49 axis=0, 50 ), 51 ) 52 for key, profile in initial_profiles.items() 53 ) 54 mpdata = MPDATA_2D( 55 advectees=advectees, 56 stream_function=self.settings.stream_function, 57 rhod_of_zZ=self.settings.rhod_of_zZ, 58 dt=self.settings.dt, 59 grid=self.settings.grid, 60 size=self.settings.size, 61 n_iters=self.settings.mpdata_iters, 62 infinite_gauge=self.settings.mpdata_iga, 63 nonoscillatory=self.settings.mpdata_fct, 64 third_order_terms=self.settings.mpdata_tot, 65 ) 66 environment = Kinematic2D( 67 dt=self.settings.dt, 68 grid=self.settings.grid, 69 size=self.settings.size, 70 rhod_of=self.settings.rhod_of_zZ, 71 mixed_phase=self.settings.processes["freezing"], 72 backend=backend, 73 solvers=mpdata, 74 ) 75 76 dynamics = [] 77 78 if products is not None: 79 products = list(products) 80 else: 81 products = make_default_product_collection(self.settings) 82 83 if self.settings.processes["fluid advection"]: 84 dynamics.append(AmbientThermodynamics()) 85 if self.settings.processes["condensation"]: 86 kwargs = {} 87 if not self.settings.condensation_adaptive: 88 kwargs["substeps"] = (self.settings.condensation_substeps,) 89 condensation = Condensation( 90 rtol_x=self.settings.condensation_rtol_x, 91 rtol_thd=self.settings.condensation_rtol_thd, 92 adaptive=self.settings.condensation_adaptive, 93 dt_cond_range=self.settings.condensation_dt_cond_range, 94 schedule=self.settings.condensation_schedule, 95 **kwargs, 96 ) 97 dynamics.append(condensation) 98 if self.settings.processes["fluid advection"]: 99 dynamics.append(EulerianAdvection()) 100 if self.settings.processes["particle advection"]: 101 dynamics.append( 102 Displacement( 103 enable_sedimentation=self.settings.processes["sedimentation"], 104 adaptive=self.settings.displacement_adaptive, 105 rtol=self.settings.displacement_rtol, 106 ) 107 ) 108 if ( 109 self.settings.processes["coalescence"] 110 and self.settings.processes["breakup"] 111 ): 112 dynamics.append( 113 Collision( 114 collision_kernel=self.settings.kernel, 115 enable_breakup=self.settings.processes["breakup"], 116 coalescence_efficiency=self.settings.coalescence_efficiency, 117 breakup_efficiency=self.settings.breakup_efficiency, 118 fragmentation_function=self.settings.breakup_fragmentation, 119 adaptive=self.settings.coalescence_adaptive, 120 dt_coal_range=self.settings.coalescence_dt_coal_range, 121 substeps=self.settings.coalescence_substeps, 122 optimized_random=self.settings.coalescence_optimized_random, 123 ) 124 ) 125 elif ( 126 self.settings.processes["coalescence"] 127 and not self.settings.processes["breakup"] 128 ): 129 dynamics.append( 130 Coalescence( 131 collision_kernel=self.settings.kernel, 132 adaptive=self.settings.coalescence_adaptive, 133 dt_coal_range=self.settings.coalescence_dt_coal_range, 134 substeps=self.settings.coalescence_substeps, 135 optimized_random=self.settings.coalescence_optimized_random, 136 ) 137 ) 138 assert not ( 139 self.settings.processes["breakup"] 140 and not self.settings.processes["coalescence"] 141 ) 142 if self.settings.processes["freezing"]: 143 dynamics.append( 144 Freezing( 145 immersion_freezing=self.settings.freezing_immersion, 146 thaw=self.settings.freezing_thaw, 147 ) 148 ) 149 150 attributes = environment.init_attributes( 151 spatial_discretisation=spatial_sampling.Pseudorandom(), 152 dry_radius_spectrum=self.settings.spectrum_per_mass_of_dry_air, 153 kappa=self.settings.kappa, 154 n_sd=self.settings.n_sd 155 // (2 if self.settings.freezing_inp_frac != 1 else 1), 156 ) 157 158 if self.settings.processes["freezing"]: 159 attributes["signed water mass"] = attributes.pop("water mass") 160 161 if self.settings.freezing_inp_spec is None: 162 immersed_surface_area = formulae.trivia.sphere_surface( 163 diameter=2 * formulae.trivia.radius(volume=attributes["dry volume"]) 164 ) 165 else: 166 immersed_surface_area = self.settings.freezing_inp_spec.percentiles( 167 np.random.random(attributes["dry volume"].size), # TODO #599: seed 168 ) 169 170 if self.settings.freezing_immersion == "singular": 171 attributes["freezing temperature"] = ( 172 formulae.freezing_temperature_spectrum.invcdf( 173 np.random.random(immersed_surface_area.size), # TODO #599: seed 174 immersed_surface_area, 175 ) 176 ) 177 else: 178 attributes["immersed surface area"] = immersed_surface_area 179 180 if self.settings.freezing_inp_frac != 1: 181 assert self.settings.n_sd % 2 == 0 182 assert 0 < self.settings.freezing_inp_frac < 1 183 freezing_attribute = { 184 "singular": "freezing temperature", 185 "time-dependent": "immersed surface area", 186 }[self.settings.freezing_immersion] 187 for name, array in attributes.items(): 188 if array.shape[-1] != self.settings.n_sd // 2: 189 raise AssertionError(f"attribute >>{name}<< has wrong size") 190 array = array.copy() 191 full_shape = list(array.shape) 192 orig = slice(None, full_shape[-1]) 193 copy = slice(orig.stop, None) 194 full_shape[-1] *= 2 195 attributes[name] = np.empty(full_shape, dtype=array.dtype) 196 if name == freezing_attribute: 197 attributes[name][orig] = array 198 attributes[name][copy] = 0 199 elif name == "multiplicity": 200 attributes[name][orig] = array * self.settings.freezing_inp_frac 201 attributes[name][copy] = array * ( 202 1 - self.settings.freezing_inp_frac 203 ) 204 elif len(array.shape) > 1: # particle positions 205 # TODO #599: seed 206 for dim, _ in enumerate(array.shape): 207 # only to make particles not shadow each other in visualisations 208 attributes[name][dim, orig] = array[dim, :] 209 attributes[name][dim, copy] = np.random.permutation( 210 array[dim, :] 211 ) 212 else: 213 attributes[name][orig] = array 214 attributes[name][copy] = array 215 216 non_zero_per_gridbox = np.count_nonzero( 217 attributes[freezing_attribute] 218 ) / np.prod(self.settings.grid) 219 assert non_zero_per_gridbox == self.settings.n_sd_per_gridbox / 2 220 221 self.particulator = Particulator( 222 n_sd=self.settings.n_sd, 223 environment=environment, 224 dynamics=dynamics, 225 attributes=attributes, 226 products=products, 227 ) 228 229 if self.SpinUp is not None: 230 self.SpinUp(self.particulator, self.settings.n_spin_up) 231 if self.storage is not None: 232 self.storage.init(self.settings) 233 234 def run(self, controller=DummyController(), vtk_exporter=None): 235 with controller: 236 for step in self.settings.output_steps: 237 if controller.panic: 238 break 239 240 self.particulator.advance(step - self.particulator.n_steps) 241 242 self.store(step) 243 244 if vtk_exporter is not None: 245 vtk_exporter.export_attributes(self.particulator) 246 vtk_exporter.export_products(self.particulator) 247 248 controller.set_percent(step / self.settings.output_steps[-1]) 249 250 def store(self, step): 251 for name, product in self.particulator.products.items(): 252 self.storage.save(product.get(), step, name)
def
reinit(self, products=None):
36 def reinit(self, products=None): 37 formulae = self.settings.formulae 38 backend = self.backend 39 initial_profiles = { 40 "th": self.settings.initial_dry_potential_temperature_profile, 41 "water_vapour_mixing_ratio": self.settings.initial_vapour_mixing_ratio_profile, 42 } 43 advectees = dict( 44 ( 45 key, 46 np.repeat( 47 profile.reshape(1, -1), 48 self.settings.grid[0], 49 axis=0, 50 ), 51 ) 52 for key, profile in initial_profiles.items() 53 ) 54 mpdata = MPDATA_2D( 55 advectees=advectees, 56 stream_function=self.settings.stream_function, 57 rhod_of_zZ=self.settings.rhod_of_zZ, 58 dt=self.settings.dt, 59 grid=self.settings.grid, 60 size=self.settings.size, 61 n_iters=self.settings.mpdata_iters, 62 infinite_gauge=self.settings.mpdata_iga, 63 nonoscillatory=self.settings.mpdata_fct, 64 third_order_terms=self.settings.mpdata_tot, 65 ) 66 environment = Kinematic2D( 67 dt=self.settings.dt, 68 grid=self.settings.grid, 69 size=self.settings.size, 70 rhod_of=self.settings.rhod_of_zZ, 71 mixed_phase=self.settings.processes["freezing"], 72 backend=backend, 73 solvers=mpdata, 74 ) 75 76 dynamics = [] 77 78 if products is not None: 79 products = list(products) 80 else: 81 products = make_default_product_collection(self.settings) 82 83 if self.settings.processes["fluid advection"]: 84 dynamics.append(AmbientThermodynamics()) 85 if self.settings.processes["condensation"]: 86 kwargs = {} 87 if not self.settings.condensation_adaptive: 88 kwargs["substeps"] = (self.settings.condensation_substeps,) 89 condensation = Condensation( 90 rtol_x=self.settings.condensation_rtol_x, 91 rtol_thd=self.settings.condensation_rtol_thd, 92 adaptive=self.settings.condensation_adaptive, 93 dt_cond_range=self.settings.condensation_dt_cond_range, 94 schedule=self.settings.condensation_schedule, 95 **kwargs, 96 ) 97 dynamics.append(condensation) 98 if self.settings.processes["fluid advection"]: 99 dynamics.append(EulerianAdvection()) 100 if self.settings.processes["particle advection"]: 101 dynamics.append( 102 Displacement( 103 enable_sedimentation=self.settings.processes["sedimentation"], 104 adaptive=self.settings.displacement_adaptive, 105 rtol=self.settings.displacement_rtol, 106 ) 107 ) 108 if ( 109 self.settings.processes["coalescence"] 110 and self.settings.processes["breakup"] 111 ): 112 dynamics.append( 113 Collision( 114 collision_kernel=self.settings.kernel, 115 enable_breakup=self.settings.processes["breakup"], 116 coalescence_efficiency=self.settings.coalescence_efficiency, 117 breakup_efficiency=self.settings.breakup_efficiency, 118 fragmentation_function=self.settings.breakup_fragmentation, 119 adaptive=self.settings.coalescence_adaptive, 120 dt_coal_range=self.settings.coalescence_dt_coal_range, 121 substeps=self.settings.coalescence_substeps, 122 optimized_random=self.settings.coalescence_optimized_random, 123 ) 124 ) 125 elif ( 126 self.settings.processes["coalescence"] 127 and not self.settings.processes["breakup"] 128 ): 129 dynamics.append( 130 Coalescence( 131 collision_kernel=self.settings.kernel, 132 adaptive=self.settings.coalescence_adaptive, 133 dt_coal_range=self.settings.coalescence_dt_coal_range, 134 substeps=self.settings.coalescence_substeps, 135 optimized_random=self.settings.coalescence_optimized_random, 136 ) 137 ) 138 assert not ( 139 self.settings.processes["breakup"] 140 and not self.settings.processes["coalescence"] 141 ) 142 if self.settings.processes["freezing"]: 143 dynamics.append( 144 Freezing( 145 immersion_freezing=self.settings.freezing_immersion, 146 thaw=self.settings.freezing_thaw, 147 ) 148 ) 149 150 attributes = environment.init_attributes( 151 spatial_discretisation=spatial_sampling.Pseudorandom(), 152 dry_radius_spectrum=self.settings.spectrum_per_mass_of_dry_air, 153 kappa=self.settings.kappa, 154 n_sd=self.settings.n_sd 155 // (2 if self.settings.freezing_inp_frac != 1 else 1), 156 ) 157 158 if self.settings.processes["freezing"]: 159 attributes["signed water mass"] = attributes.pop("water mass") 160 161 if self.settings.freezing_inp_spec is None: 162 immersed_surface_area = formulae.trivia.sphere_surface( 163 diameter=2 * formulae.trivia.radius(volume=attributes["dry volume"]) 164 ) 165 else: 166 immersed_surface_area = self.settings.freezing_inp_spec.percentiles( 167 np.random.random(attributes["dry volume"].size), # TODO #599: seed 168 ) 169 170 if self.settings.freezing_immersion == "singular": 171 attributes["freezing temperature"] = ( 172 formulae.freezing_temperature_spectrum.invcdf( 173 np.random.random(immersed_surface_area.size), # TODO #599: seed 174 immersed_surface_area, 175 ) 176 ) 177 else: 178 attributes["immersed surface area"] = immersed_surface_area 179 180 if self.settings.freezing_inp_frac != 1: 181 assert self.settings.n_sd % 2 == 0 182 assert 0 < self.settings.freezing_inp_frac < 1 183 freezing_attribute = { 184 "singular": "freezing temperature", 185 "time-dependent": "immersed surface area", 186 }[self.settings.freezing_immersion] 187 for name, array in attributes.items(): 188 if array.shape[-1] != self.settings.n_sd // 2: 189 raise AssertionError(f"attribute >>{name}<< has wrong size") 190 array = array.copy() 191 full_shape = list(array.shape) 192 orig = slice(None, full_shape[-1]) 193 copy = slice(orig.stop, None) 194 full_shape[-1] *= 2 195 attributes[name] = np.empty(full_shape, dtype=array.dtype) 196 if name == freezing_attribute: 197 attributes[name][orig] = array 198 attributes[name][copy] = 0 199 elif name == "multiplicity": 200 attributes[name][orig] = array * self.settings.freezing_inp_frac 201 attributes[name][copy] = array * ( 202 1 - self.settings.freezing_inp_frac 203 ) 204 elif len(array.shape) > 1: # particle positions 205 # TODO #599: seed 206 for dim, _ in enumerate(array.shape): 207 # only to make particles not shadow each other in visualisations 208 attributes[name][dim, orig] = array[dim, :] 209 attributes[name][dim, copy] = np.random.permutation( 210 array[dim, :] 211 ) 212 else: 213 attributes[name][orig] = array 214 attributes[name][copy] = array 215 216 non_zero_per_gridbox = np.count_nonzero( 217 attributes[freezing_attribute] 218 ) / np.prod(self.settings.grid) 219 assert non_zero_per_gridbox == self.settings.n_sd_per_gridbox / 2 220 221 self.particulator = Particulator( 222 n_sd=self.settings.n_sd, 223 environment=environment, 224 dynamics=dynamics, 225 attributes=attributes, 226 products=products, 227 ) 228 229 if self.SpinUp is not None: 230 self.SpinUp(self.particulator, self.settings.n_spin_up) 231 if self.storage is not None: 232 self.storage.init(self.settings)
def
run( self, controller=<PySDM_examples.utils.dummy_controller.DummyController object>, vtk_exporter=None):
234 def run(self, controller=DummyController(), vtk_exporter=None): 235 with controller: 236 for step in self.settings.output_steps: 237 if controller.panic: 238 break 239 240 self.particulator.advance(step - self.particulator.n_steps) 241 242 self.store(step) 243 244 if vtk_exporter is not None: 245 vtk_exporter.export_attributes(self.particulator) 246 vtk_exporter.export_products(self.particulator) 247 248 controller.set_percent(step / self.settings.output_steps[-1])