def _update_plots(self): """ Update plots requested in input (reporting/reports_to_show) """ if self.simulation.rank != 0: return # Do not plot on non root processes for report_name in self.figures: if not report_name in self.timestep_xy_reports: ocellaris_error( 'Unknown report name: "%s"' % report_name, 'Cannot plot this report, it does not exist', ) abscissa = self.timesteps ordinate = self.timestep_xy_reports[report_name] if len(abscissa) != len(ordinate): ocellaris_error( 'Malformed report data: "%s"' % report_name, 'Length of t is %d while report is %d' % (len(abscissa), len(ordinate)), ) fig, ax, line = self.figures[report_name] line.set_xdata(abscissa) line.set_ydata(ordinate) ax.relim() ax.autoscale_view() fig.canvas.draw() fig.canvas.flush_events()
def update(self, dt, velocity): """ Update the values of the blending function beta at the facets according to the HRIC algorithm. Several versions of HRIC are implemented """ degree_b = self.blending_function.ufl_element().degree() degree_u = velocity[0].ufl_element().degree() assert degree_b == 0, ( 'Only facetwise constant blending factors are supported! Got order %d' % degree_b) assert degree_u == 0, ( 'VelocityDGT0Projector must be enabled! Got order %d' % degree_u) # Check that the input is supported by the C++ code degree_a = self.alpha_function.ufl_element().degree() if degree_a != 0: ocellaris_error( 'HRIC scalar field order must be 0', 'HRIC implementation does not support order %d fields' % degree_a, ) with dolfin.Timer('Ocellaris update HRIC'): if self.use_cpp: Co_max = self.update_cpp(dt, velocity) else: Co_max = self.update_python(dt, velocity) Co_max = dolfin.MPI.max(self.mesh.mpi_comm(), Co_max) self.simulation.reporting.report_timestep_value('Cof_max', Co_max)
def get_variable(self, name): if not name == self.var_name: ocellaris_error( 'Sharp field does not define %r' % name, 'This sharp field defines %r' % self.var_name, ) return self.func
def morph_mesh(simulation, columns, fs_vert_velocity): """ Move the mesh. Each column is given a velocity in the vertical direction. All vertices in this column is moved according to the """ assert len(columns) == len(fs_vert_velocity) dt = simulation.dt u_mesh = simulation.data['u_mesh1'] for col, vel in zip(columns, fs_vert_velocity): y_fs = col.free_surface_pos for _vid, coords, dof in col.vertices: y_vtx = coords[1] if y_vtx > y_fs: fac = (col.top - y_vtx) / (col.top - y_fs) else: fac = (y_vtx - col.bottom) / (y_fs - col.bottom) u_mesh.vector()[dof] = vel * fac col.free_surface_pos = y_fs + vel * dt if col.free_surface_pos >= col.top: ocellaris_error( 'HeightFunctionALE morphing error', 'Free surface is above top of column in column %r' % col, ) simulation.mesh_morpher.morph_mesh()
def report(self, create_report=True): """ Compute and report the solution properties """ if not self.active: return sim = self.simulation reports = [] Co_max = self.courant_number().vector().max() Pe_max = self.peclet_number().vector().max() div_dS_f, div_dx_f = self.divergences() div_dS = div_dS_f.vector().max() div_dx = div_dx_f.vector().max() mass = self.total_mass() Ek, Ep = self.total_energy() reports.append(('Co', Co_max)) reports.append(('Pe', Pe_max)) reports.append(('div', div_dx + div_dS)) reports.append(('mass', mass)) reports.append(('Ek', Ek)) reports.append(('Ep', Ep)) if self.has_div_conv: # Convecting and convected velocities are separate div_conv_dS_f, div_conv_dx_f = self.divergences('u_conv') div_conv_dS = div_conv_dS_f.vector().max() div_conv_dx = div_conv_dx_f.vector().max() reports.append(('div_conv', div_conv_dx + div_conv_dS)) # Difference between the convective and the convected velocity ucdiff = velocity_change(u1=sim.data['up'], u2=sim.data['up_conv'], ui_tmp=sim.data['ui_tmp']) reports.append(('uconv_diff', ucdiff)) if create_report: # Add computed solution properties to the timestep reports for name, value in reports: sim.reporting.report_timestep_value(name, value) else: # Log solution properties instead of adding to the timestep reports. # Done to avoid variable length time step report time series which # will be generated when calling this method at a different time # than at the end of a time step info = ['%s = %10g' % (name, value) for name, value in reports] sim.log.info( 'Solution properties for timestep = %5d, time = %10.4f, %s' % (sim.timestep, sim.time, ', '.join(info))) Co_lim = sim.input.get_value('simulation/Co_lim', CO_LIM, 'float') if Co_lim > 0 and Co_max > Co_lim: ocellaris_error('Courant number exceeded limit', 'Found Co = %g > %g' % (Co_max, Co_lim)) elif not numpy.isfinite(Co_max): ocellaris_error('Non finite Courant number', 'Found Co = %g' % Co_max)
def taylor_to_lagrange(t, u): """ Convert a DG Taylor function space function into a Lagrange function space function. The results are stored in u which should be a function space with an appropriate number of dofs per cell (u in DG2 if t in DG2 etc) """ V = u.function_space() mesh = V.mesh() degree = V.ufl_element().degree() ndim = mesh.geometry().dim() if 'mesh_hash' not in CACHE or CACHE['mesh_hash'] != mesh.hash(): CACHE.clear() CACHE['mesh'] = mesh CACHE['mesh_hash'] = mesh.hash() # Get cached conversion matrices key = ('taylor_to_lagrange_matrices', degree) if key not in CACHE: if degree == 1 and ndim == 2: CACHE[key] = taylor_to_DG1_matrix_2D(V) elif degree == 1 and ndim == 3: CACHE[key] = taylor_to_DG1_matrix_3D(V) elif degree == 2 and ndim == 2: CACHE[key] = taylor_to_DG2_matrix_2D(V) elif degree == 2 and ndim == 3: CACHE[key] = taylor_to_DG2_matrix_3D(V) else: ocellaris_error( 'DG Taylor to DG Lagrange converter error', 'Polynomial degree %d not supported' % degree, ) taylor_to_lagrange_matrices = CACHE[key] Ncells = taylor_to_lagrange_matrices.shape[0] # Get cached cell dofs key2 = ('cell_dofs', degree) if key2 not in CACHE: dm = V.dofmap() cell_dofs = [dm.cell_dofs(i) for i in range(Ncells)] CACHE[key2] = numpy.array(cell_dofs, int) cell_dofs = CACHE[key2] # Apply the conversion matrices for all cells by use of the stacked dot # behaviour of matmul (slightly faster than einsum 'ijk,ik->ij') all_vals_taylor = t.vector().get_local() taylor_vectors = all_vals_taylor.take(cell_dofs) res = numpy.matmul( taylor_to_lagrange_matrices, taylor_vectors[:, :, None] ).squeeze() # Put the results into the right indices in the Taylor function's vector all_vals_lagrange = numpy.zeros_like(all_vals_taylor) all_vals_lagrange[cell_dofs] = res u.vector().set_local(all_vals_lagrange) u.vector().apply('insert')
def get_variable(self, name): if not name == self.var_name: ocellaris_error( 'Scalar field does not define %r' % name, 'This scalar field defines %r' % self.var_name, ) if self.func is None: expr = self._get_expression() func = dolfin.Function(self.V) func.interpolate(expr) self.func = func return self.func
def _interp(self, name, expr=None, func=None): """ Interpolate the expression into the given function """ sim = self.simulation # Determine the function space V = self.V quad_degree = None if name == 'c' and self.colour_projection_degree >= 0: # Perform projection for c instead of interpolation to better # capture the discontinuous nature of the colour field if 'Vc' not in sim.data: ocellaris_error( 'Error in wave field %r input' % self.name, 'Cannot specify colour_to_dg_degree when c is ' 'not used in the multiphase_solver.', ) V = sim.data['Vc'] quad_degree = self.colour_projection_degree # Get the expression if expr is None: expr = self._get_expression(name, quad_degree) # Get the function if func is None: if name not in self._functions: self._functions[name] = dolfin.Function(V) func = self._functions[name] if quad_degree is not None: if self.colour_projection_form is None: # Ensure that we can use the DG0 trick of dividing by the mass # matrix diagonal to get the projected value if V.ufl_element().degree() != 0: ocellaris_error( 'Error in wave field %r input' % self.name, 'The colour_to_dg_degree projection is ' 'currently only implemented when c is DG0', ) v = dolfin.TestFunction(V) dx = dolfin.dx(metadata={'quadrature_degree': quad_degree}) d = dolfin.CellVolume(V.mesh()) # mass matrix diagonal form = expr * v / d * dx self.colour_projection_form = dolfin.Form(form) # Perform projection by assembly (DG0 only!) dolfin.assemble(self.colour_projection_form, tensor=func.vector()) else: # Perform standard interpolation func.interpolate(expr)
def get_probe(name): """ Return a postprocessing probe by name """ try: return _PROBES[name] except KeyError: ocellaris_error( 'Postprocessing probe "%s" not found' % name, 'Available probe:\n' + '\n'.join(' %-20s - %s' % (n, s.description) for n, s in sorted(_PROBES.items())), ) raise
def get_variable(self, name): if not name == self.var_name: ocellaris_error( 'Vector field does not define %r' % name, 'This vector field defines %r' % self.var_name, ) if self.funcs is None: funcs = [] for e in self._get_expressions(): func = dolfin.Function(self.V) func.interpolate(e) funcs.append(func) self.funcs = dolfin.as_vector(funcs) return self.funcs
def get_convection_scheme(name): """ Return a convection scheme by name """ try: return _CONVECTION_SCHEMES[name] except KeyError: ocellaris_error( 'Convection scheme "%s" not found' % name, 'Available convection schemes:\n' + '\n'.join(' %-20s - %s' % (n, s.description) for n, s in sorted(_CONVECTION_SCHEMES.items())), ) raise
def get_multi_phase_model(name): """ Return a multi phase model by name """ try: return _MULTI_PHASE_MODELS[name] except KeyError: ocellaris_error( 'Multi phase model "%s" not found' % name, 'Available models:\n' + '\n'.join(' %-20s - %s' % (n, s.description) for n, s in sorted(_MULTI_PHASE_MODELS.items())), ) raise
def get_variable(self, name): if not name == self.var_name: ocellaris_error( 'Scalar field does not define %r' % name, 'This scalar field defines %r' % self.var_name, ) if self.func is not None: return self.func if self.colour_projection_degree >= 0: quad_degree = self.colour_projection_degree degree = None else: quad_degree = None degree = self.polydeg expr = OcellarisCppExpression( self.simulation, self.cpp_code, 'Scalar field %r' % self.name, degree=degree, update=False, quad_degree=quad_degree, ) self.func = dolfin.Function(self.V) if quad_degree is not None: # Ensure that we can use the DG0 trick of dividing by the mass # matrix diagonal to get the projected value if self.V.ufl_element().degree() != 0: ocellaris_error( 'Error in wave field %r input' % self.name, 'The colour_to_dg_degree projection is ' 'currently only implemented when c is DG0', ) v = dolfin.TestFunction(self.V) dx = dolfin.dx(metadata={'quadrature_degree': quad_degree}) d = dolfin.CellVolume(self.V.mesh()) # mass matrix diagonal form = expr * v / d * dx compiled_form = dolfin.Form(form) # Perform projection by assembly (DG0 only!) dolfin.assemble(compiled_form, tensor=self.func.vector()) else: # Perform standard interpolation self.func.interpolate(expr) return self.func
def check_isinstance(value, classes): """ Give error if the input data is not of the required type """ value = eval_python_expression(self.simulation, value, pathstr, safe_mode) if not isinstance(value, classes): ocellaris_error( 'Malformed data on input file', 'Parameter %s should be of type %s,\nfound %r %r' % (pathstr, required_type, value, type(value)), ) return value
def get_slope_limiter(name): """ Return a slope limiter by name """ try: return _SLOPE_LIMITERS[name] except KeyError: ocellaris_error( 'Slope limiter "%s" not found' % name, 'Available slope limiters:\n' + '\n'.join(' %-20s - %s' % (n, s.description) for n, s in sorted(_SLOPE_LIMITERS.items())), ) raise
def get_known_field(name): """ Return a known field by name """ try: return _KNOWN_FIELDS[name] except KeyError: ocellaris_error( 'Field type "%s" not found' % name, 'Available field types:\n' + '\n'.join(' %-20s - %s' % (n, s.description) for n, s in sorted(_KNOWN_FIELDS.items())), ) raise
def get_solver(name): """ Return a Navier-Stokes solver by name """ try: return _SOLVERS[name] except KeyError: ocellaris_error( 'Navier-Stokes solver "%s" not found' % name, 'Available solvers:\n' + '\n'.join(' %-20s - %s' % (n, s.description) for n, s in sorted(_SOLVERS.items())), ) raise
def get_boundary_condition(name): """ Return a boundary condition by name """ try: return _BOUNDARY_CONDITIONS[name] except KeyError: ocellaris_error( 'Boundary condition "%s" not found' % name, 'Available boundary conditions:\n' + '\n'.join(' %-20s - %s' % (n, s.description) for n, s in sorted(_BOUNDARY_CONDITIONS.items())), ) raise
def read_input(self, field_inp): sim = self.simulation self.wave_model = field_inp.get_value('wave_model', 'Fenton', 'string') self.air_model = field_inp.get_value('air_model', 'FentonAir', 'string') self.order = field_inp.get_value('model_order', 5, 'int') # Get global physical constants g = abs(sim.data['g'].values()[-1]) if g == 0: ocellaris_error('Waves require gravity', 'The vertical component of gravity is 0') h = field_inp.get_value('depth', required_type='float') if h <= 0: ocellaris_error('Waves require a still water depth', 'The still water depth is %r' % h) self.wave_length = field_inp.get_value('wave_length', required_type='float') self.wave_height = field_inp.get_value('wave_height', required_type='float') self.stationary = self.wave_height == 0 self.ramp_time = field_inp.get_value('ramp_time', 0, required_type='float') self.still_water_pos = field_inp.get_value('still_water_position', required_type='float') self.current_speed = field_inp.get_value('current_speed', 0, required_type='float') self.wind_speed = field_inp.get_value('wind_speed', 0, required_type='float') self.polydeg = field_inp.get_value('polynomial_degree', DEFAULT_POLYDEG, required_type='int') self.g = g self.h = h h_above = 3 * self.wave_height self.h_above = field_inp.get_value('depth_above', h_above, required_type='float') self.blending_height = field_inp.get_value('blending_height', None, 'float') # Project the colour function to DG0 (set degree to -1 to prevent this) self.colour_projection_degree = field_inp.get_value( 'colour_projection_degree', COLOUR_PROJECTION_DEGREE, 'int') self.colour_projection_form = None
def read_input(self): """ Read the simulation input """ sim = self.simulation # Solver for the coupled system default_lu_solver = LU_SOLVER_1CPU if sim.ncpu == 1 else LU_SOLVER_NCPU self.coupled_solver = linear_solver_from_input( sim, 'solver/coupled', 'lu', None, default_lu_solver, LU_PARAMETERS ) # Give warning if using iterative solver if isinstance(self.coupled_solver, dolfin.PETScKrylovSolver): sim.log.warning( 'WARNING: Using a Krylov solver for the coupled NS equations is not a good idea' ) else: # Removed in DOLFIN 2018.1 # self.coupled_solver.set_parameter('same_nonzero_pattern', True) pass # Deal with pressure null space self.fix_pressure_dof = sim.input.get_value( 'solver/fix_pressure_dof', FIX_PRESSURE_DOF, 'bool' ) # No need for any tricks if the pressure is set via Dirichlet conditions somewhere if self.simulation.data['dirichlet_bcs'].get('p', []): self.fix_pressure_dof = False # Representation of velocity Vu_family = sim.data['Vu'].ufl_element().family() Vp_family = sim.data['Vp'].ufl_element().family() if not Vu_family == Vp_family == 'Discontinuous Lagrange': ocellaris_error( 'Must use DG function spaces', 'LDG solver requires DG spaces for velocity and pressure', ) # Local DG velocity postprocessing self.velocity_postprocessing_method = sim.input.get_value( 'solver/velocity_postprocessing', BDM, 'string' ) # Quasi-steady simulation input self.steady_velocity_eps = sim.input.get_value( 'solver/steady_velocity_stopping_criterion', None, 'float' ) self.is_steady = self.steady_velocity_eps is not None
def setup_fenics(simulation): """ Setup FEniCS parameters like linear algebra backend """ # Test for PETSc linear algebra backend if not dolfin.has_linear_algebra_backend("PETSc"): ocellaris_error( 'Missing PETSc', 'DOLFIN has not been configured with PETSc ' 'which is needed by Ocellaris.', ) dolfin.parameters['linear_algebra_backend'] = 'PETSc' # Form compiler "uflacs" needed for isoparametric elements form_compiler = simulation.input.get_value('solver/form_compiler', 'auto', 'string') dolfin.parameters['form_compiler']['representation'] = form_compiler
def check_list(d, valtype): """ Check list and eval any python expressions in the values """ d = check_isinstance(d, list) d_new = [] for val in d: d_new.append(check_isinstance(val, valtype)) if required_length is not None and len(d_new) != required_length: ocellaris_error( 'Malformed data on input file', 'Parameter %s should be length %r found %r' % (pathstr, required_length, len(d_new)), ) return d_new
def calc_wave_number(g, h, omega, relax=0.5, eps=1e-15): """ Relaxed Picard iterations to find k when omega is known """ k0 = omega ** 2 / g for _ in range(100): k1 = omega ** 2 / g / tanh(k0 * h) if abs(k1 - k0) < eps: break k0 = k1 * relax + k0 * (1 - relax) else: ocellaris_error( 'calc_wave_number did not converge', 'Input g=%r h=%r omega=%r, tolerance=%e' % (g, h, omega, eps), ) return k1
def eval_python_expression(simulation, value, pathstr, safe_mode=False): """ We run eval with the math functions and user constants available on string values that are prefixed with "py$" indicating that they are dynamic expressions and not just static strings """ if not isinstance(value, str) or not value.startswith('py$'): return value if safe_mode: ocellaris_error( 'Cannot have Python expression here', 'Not allowed to have Python expression here: %s' % pathstr, ) # remove "py$" prefix expr = value[3:] # Build dictionary of locals for evaluating the expression eval_locals = {} import math for name in dir(math): if not name.startswith('_'): eval_locals[name] = getattr(math, name) global_inp = simulation.input user_constants = global_inp.get_value('user_code/constants', {}, 'dict(string:basic)', safe_mode=True) for name, const_value in user_constants.items(): eval_locals[name] = const_value eval_locals['simulation'] = simulation eval_locals['t'] = eval_locals['time'] = simulation.time eval_locals['it'] = eval_locals['timestep'] = simulation.timestep eval_locals['dt'] = simulation.dt eval_locals['ndim'] = simulation.ndim try: value = eval(expr, globals(), eval_locals) except Exception: simulation.log.error('Cannot evaluate python code for %s' % pathstr) simulation.log.error('Python code is %s' % expr) raise return value
def setup_known_fields(simulation): """ Setup known fields like incoming waves etc """ simulation.log.info('Creating known fields') Nfields = len(simulation.input.get_value('fields', [], 'list(dict)')) for i in range(Nfields): field_inp = simulation.input.get_value('fields/%d' % i, required_type='Input') name = field_inp.get_value('name', required_type='string') field_type = field_inp.get_value('type', required_type='string') if name in simulation.fields: ocellaris_error( 'Field %s is defined multiple times' % name, 'Input file contains multiple fields with the same name', ) field_class = get_known_field(field_type) simulation.fields[name] = field_class(simulation, field_inp)
def read_input(self, field_inp): self.name = field_inp.get_value('name', required_type='string') self.var_name = field_inp.get_value('variable_name', 'phi', required_type='string') self.stationary = field_inp.get_value('stationary', False, required_type='bool') self.polydeg = field_inp.get_value('polynomial_degree', DEFAULT_POLYDEG, required_type='int') self.cpp_code = field_inp.get_value('cpp_code', required_type='list(string)') if len(self.cpp_code) != self.simulation.ndim: ocellaris_error( 'Wrong number of dimensions in cpp code for field %r' % self.name, 'Simulation has %d dimensions, cpp_code length is %d' % (self.simulation.ndim, len(self.cpp_code)), )
def get_input_file_path(self, file_name): """ Serch first relative to the current working dir and then relative to the input file dir """ # Check if the path is absolute or relative to the # working directory if os.path.exists(file_name): return file_name self.simulation.log.debug('File does not exist: %s' % file_name) # Check if the path is relative to the input file dir inp_file_dir = os.path.dirname(self.file_name) pth2 = os.path.join(inp_file_dir, file_name) if os.path.exists(pth2): return pth2 self.simulation.log.debug('File does not exist: %s' % pth2) ocellaris_error('File not found', 'The specified file "%s" was not found' % file_name)
def read_metadata(self, h5_file_name, function_details=False): """ Read HDF5 restart file metadata """ # Check file format and read metadata with h5py.File(h5_file_name, 'r') as hdf: if not 'ocellaris' in hdf: ocellaris_error( 'Error reading restart file', 'Restart file %r does not contain Ocellaris meta data' % h5_file_name, ) meta = hdf['ocellaris'] restart_file_version = meta.attrs['restart_file_format'] if restart_file_version != 2: ocellaris_error( 'Error reading restart file', 'Restart file version is %d, this version of Ocellaris only ' % restart_file_version + 'supports version 2', ) t = float(meta.attrs['time']) it = int(meta.attrs['iteration']) dt = float(meta.attrs['dt']) inpdata = meta['input_file'].value funcnames = list(meta['function_names']) # Read function signatures if function_details: signatures = {} for fname in funcnames: signatures[fname] = hdf[fname].attrs['signature'] if function_details: return funcnames, signatures else: return t, it, dt, inpdata, funcnames
def create_periodic_boundary_conditions(self): """ Create periodic boundary conditions for this region This is separated from the normal boundary conditions because Dirichlet boundary conditions depend on having function spaces created, while the function spaces themselves are dependent on the definition of periodic boundaries. """ sim = self.simulation if 'map_code' in self.input: sim.log.info('Applying periodic boundary conditions on %s' % self.name) if self.simulation.data['constrained_domain'] is not None: ocellaris_error( 'Error in specification of periodic boundary conditions', 'There can only be one periodic boundary region in the domain. ' 'Found more than one periodic region when processing boundary conditions', ) self.selector.set_map_code(self.input['map_code'], self.name) self.simulation.data['constrained_domain'] = self.selector
def __init__(self, simulation, vel_u, vel_name, vel_w=None, use_cpp=True): """ Use a standard slope limiter on each component of the velocity field """ self.simulation = simulation self.vel_name = vel_name inp = simulation.input.get_value('slope_limiter/%s' % vel_name, {}, 'Input') comp_method = inp.get_value('comp_method', required_type='string') self.vel_u = vel_u self.vel_w = vel_w self.limit_conv = inp.get_value('limit_conv', False, 'bool') # Get the IsoSurface probe used to locate the free surface self.probe_name = inp.get_value('surface_probe', None, 'string') self.surface_probe = None self.limit_selected_cells_only = self.probe_name is not None # Create slope limiters dim, = vel_u.ufl_shape self.limiters = create_component_limiters( simulation, vel_name, vel_u, comp_method ) # Check that we can limit only certain cells if self.limit_selected_cells_only: for lim in self.limiters: if not hasattr(lim, 'limit_cell'): ocellaris_error( 'Cannot limit only selected cells for %s' % vel_name, 'Limiter %r does not support limiting only selected cells' % type(lim).__name__, ) simulation.hooks.add_pre_simulation_hook( self.setup, 'ComponentwiseSlopeLimiterVelocity - setup' )