def visualize(self, it=0): var = "cell_powers" try: should_vis = divmod(it, self.parameters["visualization"][var])[1] == 0 except ZeroDivisionError: should_vis = False if should_vis: if not self.up_to_date[var]: self.calculate_cell_powers() else: qfun = Function(self.DD.V0) qfun.vector()[:] = self.E qfun.rename("q", "Power") self.vis_files["cell_powers"] << (qfun, float(it)) var = "flux" try: should_vis = divmod(it, self.parameters["visualization"][var])[1] == 0 except ZeroDivisionError: should_vis = False if should_vis: if not self.up_to_date[var]: self.update_phi() for g in xrange(self.DD.G): self.vis_files["flux"][g] << (self.phi_mg[g], float(it))
def expand(self, f, target = None): """ Takes a function defined on the sub mesh and returns a function defined on the super mesh with unknown values set to zero. *Arguments* f (:class:`dolfin.Function`) The function on the sub mesh. *Returns* The function on the super mesh. """ mapping = self._get_mapping(f.function_space()) if target is None: target = Function(mapping['Vsuper']) target.rename(f.name(), f.label()) target.vector()[mapping['map']] = f.vector().array() return target
def cut(self, f, **kwargs): """ Takes a function defined on the super mesh and returns a truncated function defined on the sub mesh. *Arguments* f (:class:`dolfin.Function`) The function on the super mesh. *Returns* :class:`dolfin.Function` The function on the sub mesh. """ mapping = self._get_mapping(f.function_space()) result = Function(mapping['Vsub']) result.vector()[:] = f.vector().array()[mapping['map']] result.rename(f.name(), f.label()) return result
def expand(self, f, target=None): """ Takes a function defined on the sub mesh and returns a function defined on the super mesh with unknown values set to zero. *Arguments* f (:class:`dolfin.Function`) The function on the sub mesh. *Returns* The function on the super mesh. """ mapping = self._get_mapping(f.function_space()) if target is None: target = Function(mapping['Vsuper']) target.rename(f.name(), f.label()) target.vector()[mapping['map']] = f.vector().array() return target
def expand_vp_dolfunc(PrP, vp=None, vc=None, pc=None, pdof=None): """expand v and p to the dolfin function representation pdof = pressure dof that was set zero """ v = Function(PrP.V) p = Function(PrP.Q) if vp is not None: if vp.ndim == 1: vc = vp[:len(PrP.invinds)].reshape(len(PrP.invinds), 1) pc = vp[len(PrP.invinds):].reshape(PrP.Q.dim() - 1, 1) else: vc = vp[:len(PrP.invinds), :] pc = vp[len(PrP.invinds):, :] ve = np.zeros((PrP.V.dim(), 1)) # fill in the boundary values for bc in PrP.velbcs: bcdict = bc.get_boundary_values() ve[bcdict.keys(), 0] = bcdict.values() ve[PrP.invinds] = vc if pdof is None: pe = pc elif pdof == 0: pe = np.vstack([[0], pc]) elif pdof == -1: pe = np.vstack([pc, [0]]) else: pe = np.vstack([pc[:pdof], np.vstack([[0], pc[pdof:]])]) v.vector().set_local(ve) p.vector().set_local(pe) v.rename("v", "field") p.rename("p", "field") return v, p
def __init__(self): GMSH_EPS = 1.0e-15 # https://fenicsproject.org/qa/12891/initialize-mesh-from-vertices-connectivities-at-once points, cells, point_data, cell_data, _ = meshes.crucible_with_coils.generate( ) # Convert the cell data to 'uint' so we can pick a size_t MeshFunction # below as usual. for k0 in cell_data: for k1 in cell_data[k0]: cell_data[k0][k1] = numpy.array(cell_data[k0][k1], dtype=numpy.dtype("uint")) with TemporaryDirectory() as temp_dir: tmp_filename = os.path.join(temp_dir, "test.xml") meshio.write_points_cells( tmp_filename, points, cells, cell_data=cell_data, file_format="dolfin-xml", ) self.mesh = Mesh(tmp_filename) self.subdomains = MeshFunction( "size_t", self.mesh, os.path.join(temp_dir, "test_gmsh:physical.xml")) self.subdomain_materials = { 1: my_materials.porcelain, 2: materials.argon, 3: materials.gallium_arsenide_solid, 4: materials.gallium_arsenide_liquid, 27: materials.air, } # coils for k in range(5, 27): self.subdomain_materials[k] = my_materials.ek90 # Define the subdomains which together form a single coil. self.coil_domains = [ [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23], [24, 25, 26], ] self.wpi = 4 self.submesh_workpiece = SubMesh(self.mesh, self.subdomains, self.wpi) # http://fenicsproject.org/qa/2026/submesh-workaround-for-parallel-computation # submesh_parallel_bug_fixed = False # if submesh_parallel_bug_fixed: # submesh_workpiece = SubMesh(self.mesh, self.subdomains, self.wpi) # else: # # To get the mesh in parallel, we need to read it in from a file. # # Writing out can only happen in serial mode, though. :/ # base = os.path.join(current_path, # '../../meshes/2d/crucible-with-coils-submesh' # ) # filename = base + '.xml' # if not os.path.isfile(filename): # warnings.warn( # 'Submesh file \'{}\' does not exist. Creating... '.format( # filename # )) # if MPI.size(mpi_comm_world()) > 1: # raise RuntimeError( # 'Can only write submesh in serial mode.' # ) # submesh_workpiece = \ # SubMesh(self.mesh, self.subdomains, self.wpi) # output_stream = File(filename) # output_stream << submesh_workpiece # # Read the mesh # submesh_workpiece = Mesh(filename) coords = self.submesh_workpiece.coordinates() ymin = min(coords[:, 1]) ymax = max(coords[:, 1]) # Find the top right point. k = numpy.argmax(numpy.sum(coords, 1)) topright = coords[k, :] # Initialize mesh function for boundary domains class Left(SubDomain): def inside(self, x, on_boundary): # Explicitly exclude the lowest and the highest point of the # symmetry axis. # It is necessary for the consistency of the pressure-Poisson # system in the Navier-Stokes solver that the velocity is # exactly 0 at the boundary r>0. Hence, at the corner points # (r=0, melt-crucible, melt-crystal) we must enforce u=0 # already and cannot have a component in z-direction. return (on_boundary and x[0] < GMSH_EPS and x[1] < ymax - GMSH_EPS and x[1] > ymin + GMSH_EPS) class Crucible(SubDomain): def inside(self, x, on_boundary): return on_boundary and ( (x[0] > GMSH_EPS and x[1] < ymax - GMSH_EPS) or (x[0] > topright[0] - GMSH_EPS and x[1] > topright[1] - GMSH_EPS) or (x[0] < GMSH_EPS and x[1] < ymin + GMSH_EPS)) # At the top right part (boundary melt--gas), slip is allowed, so only # n.u=0 is enforced. Very weirdly, the PPE is consistent if and only if # the end points of UpperRight are in UpperRight. This contrasts # Left(), where the end points must NOT belong to Left(). Judging from # the experiments, these settings do the right thing. # TODO try to better understand the PPE system/dolfin's boundary # settings class Upper(SubDomain): def inside(self, x, on_boundary): return on_boundary and x[1] > ymax - GMSH_EPS class UpperRight(SubDomain): def inside(self, x, on_boundary): return (on_boundary and x[1] > ymax - GMSH_EPS and x[0] > 0.038 - GMSH_EPS) # The crystal boundary is taken to reach up to 0.038 where the # Dirichlet boundary data is about the melting point of the crystal, # 1511K. This setting gives pretty acceptable results when there is no # convection except the one induced by buoyancy. Is there is any more # stirring going on, though, the end point of the crystal with its # fixed temperature of 1511K might be the hottest point globally. This # looks rather unphysical. # TODO check out alternatives class UpperLeft(SubDomain): def inside(self, x, on_boundary): return (on_boundary and x[1] > ymax - GMSH_EPS and x[0] < 0.038 + GMSH_EPS) left = Left() crucible = Crucible() upper_left = UpperLeft() upper_right = UpperRight() self.wp_boundaries = MeshFunction( "size_t", self.submesh_workpiece, self.submesh_workpiece.topology().dim() - 1, ) self.wp_boundaries.set_all(0) left.mark(self.wp_boundaries, 1) crucible.mark(self.wp_boundaries, 2) upper_right.mark(self.wp_boundaries, 3) upper_left.mark(self.wp_boundaries, 4) if DEBUG: from dolfin import plot, interactive plot(self.wp_boundaries, title="Boundaries") interactive() submesh_boundary_indices = { "left": 1, "crucible": 2, "upper right": 3, "upper left": 4, } # Boundary conditions for the velocity. # # [1] Incompressible flow and the finite element method; volume two; # Isothermal Laminar Flow; # P.M. Gresho, R.L. Sani; # # For the choice of function space, [1] says: # "In 2D, the triangular elements P_2^+P_1 and P_2^+P_{-1} are very # good [...]. [...] If you wish to avoid bubble functions on # triangular elements, P_2P_1 is not bad, and P_2(P_1+P_0) is even # better [...]." # # It turns out that adding the bubble space significantly hampers the # convergence of the Stokes solver and also considerably increases the # time it takes to construct the Jacobian matrix of the Navier--Stokes # problem if no optimization is applied. V_element = FiniteElement("CG", self.submesh_workpiece.ufl_cell(), 2) with_bubbles = False if with_bubbles: V_element += FiniteElement("B", self.submesh_workpiece.ufl_cell(), 2) self.W_element = MixedElement(3 * [V_element]) self.W = FunctionSpace(self.submesh_workpiece, self.W_element) rot0 = Expression(("0.0", "0.0", "-2*pi*x[0] * 5.0/60.0"), degree=1) # rot0 = (0.0, 0.0, 0.0) rot1 = Expression(("0.0", "0.0", "2*pi*x[0] * 5.0/60.0"), degree=1) self.u_bcs = [ DirichletBC(self.W, rot0, crucible), DirichletBC(self.W.sub(0), 0.0, left), DirichletBC(self.W.sub(2), 0.0, left), # Make sure that u[2] is 0 at r=0. DirichletBC(self.W, rot1, upper_left), DirichletBC(self.W.sub(1), 0.0, upper_right), ] self.p_bcs = [] self.P_element = FiniteElement("CG", self.submesh_workpiece.ufl_cell(), 1) self.P = FunctionSpace(self.submesh_workpiece, self.P_element) self.Q_element = FiniteElement("CG", self.submesh_workpiece.ufl_cell(), 2) self.Q = FunctionSpace(self.submesh_workpiece, self.Q_element) # Dirichlet. # This is a bit of a tough call since the boundary conditions need to # be read from a Tecplot file here. filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/crucible-boundary.dat") data = tecplot_reader.read(filename) RZ = numpy.c_[data["ZONE T"]["node data"]["r"], data["ZONE T"]["node data"]["z"]] T_vals = data["ZONE T"]["node data"]["temp. [K]"] class TecplotDirichletBC(Expression): def eval(self, value, x): # Find on which edge x sits, and raise exception if it doesn't. edge_found = False for edge in data["ZONE T"]["element data"]: # Given a point X and an edge X0--X1, # # (1 - theta) X0 + theta X1, # # the minimum distance is assumed for # # argmin_theta ||(1-theta) X0 + theta X1 - X||^2 # = <X1 - X0, X - X0> / ||X1 - X0||^2. # # If the distance is 0 and 0<=theta<=1, we found the edge. # # Note that edges are 1-based in Tecplot. X0 = RZ[edge[0] - 1] X1 = RZ[edge[1] - 1] theta = numpy.dot(X1 - X0, x - X0) / numpy.dot( X1 - X0, X1 - X0) diff = (1.0 - theta) * X0 + theta * X1 - x if (numpy.dot(diff, diff) < 1.0e-10 and 0.0 <= theta and theta <= 1.0): # Linear interpolation of the temperature value. value[0] = (1.0 - theta) * T_vals[ edge[0] - 1] + theta * T_vals[edge[1] - 1] edge_found = True break # This class is supposed to be used for Dirichlet boundary # conditions. For some reason, FEniCS also evaluates # DirichletBC objects at coordinates which do not sit on the # boundary, see # <http://fenicsproject.org/qa/1033/dirichletbc-expressions-evaluated-away-from-the-boundary>. # The assigned values have no meaning though, so not assigning # values[0] here is okay. # # from matplotlib import pyplot as pp # pp.plot(x[0], x[1], 'xg') if not edge_found: value[0] = 0.0 if False: warnings.warn( "Coordinate ({:e}, {:e}) doesn't sit on edge.". format(x[0], x[1])) # pp.plot(RZ[:, 0], RZ[:, 1], '.k') # pp.plot(x[0], x[1], 'xr') # pp.show() # raise RuntimeError('Input coordinate ' # '{} is not on boundary.'.format(x)) return tecplot_dbc = TecplotDirichletBC(degree=5) self.theta_bcs_d = [DirichletBC(self.Q, tecplot_dbc, upper_left)] self.theta_bcs_d_strict = [ DirichletBC(self.Q, tecplot_dbc, upper_right), DirichletBC(self.Q, tecplot_dbc, crucible), DirichletBC(self.Q, tecplot_dbc, upper_left), ] # Neumann dTdr_vals = data["ZONE T"]["node data"]["dTempdx [K/m]"] dTdz_vals = data["ZONE T"]["node data"]["dTempdz [K/m]"] class TecplotNeumannBC(Expression): def eval(self, value, x): # Same problem as above: This expression is not only evaluated # at boundaries. for edge in data["ZONE T"]["element data"]: X0 = RZ[edge[0] - 1] X1 = RZ[edge[1] - 1] theta = numpy.dot(X1 - X0, x - X0) / numpy.dot( X1 - X0, X1 - X0) dist = numpy.linalg.norm((1 - theta) * X0 + theta * X1 - x) if dist < 1.0e-5 and 0.0 <= theta and theta <= 1.0: value[0] = (1 - theta) * dTdr_vals[ edge[0] - 1] + theta * dTdr_vals[edge[1] - 1] value[1] = (1 - theta) * dTdz_vals[ edge[0] - 1] + theta * dTdz_vals[edge[1] - 1] break return def value_shape(self): return (2, ) tecplot_nbc = TecplotNeumannBC(degree=5) n = FacetNormal(self.Q.mesh()) self.theta_bcs_n = { submesh_boundary_indices["upper right"]: dot(n, tecplot_nbc), submesh_boundary_indices["crucible"]: dot(n, tecplot_nbc), } self.theta_bcs_r = {} # It seems that the boundary conditions from above are inconsistent in # that solving with Dirichlet overall and mixed Dirichlet-Neumann give # different results; the value *cannot* correspond to one solution. # From looking at the solutions, the pure Dirichlet setting appears # correct, so extract the Neumann values directly from that solution. # Pick fixed coefficients roughly at the temperature that we expect. # This could be made less magic by having the coefficients depend on # theta and solving the quasilinear equation. temp_estimate = 1550.0 # Get material parameters wp_material = self.subdomain_materials[self.wpi] if isinstance(wp_material.specific_heat_capacity, float): cp = wp_material.specific_heat_capacity else: cp = wp_material.specific_heat_capacity(temp_estimate) if isinstance(wp_material.density, float): rho = wp_material.density else: rho = wp_material.density(temp_estimate) if isinstance(wp_material.thermal_conductivity, float): k = wp_material.thermal_conductivity else: k = wp_material.thermal_conductivity(temp_estimate) reference_problem = cyl_heat.Heat( self.Q, convection=None, kappa=k, rho=rho, cp=cp, source=Constant(0.0), dirichlet_bcs=self.theta_bcs_d_strict, ) theta_reference = reference_problem.solve_stationary() theta_reference.rename("theta", "temperature (Dirichlet)") # Create equivalent boundary conditions from theta_ref. This # makes sure that the potentially expensive Expression evaluation in # theta_bcs_* is replaced by something reasonably cheap. self.theta_bcs_d = [ DirichletBC(bc.function_space(), theta_reference, bc.domain_args[0]) for bc in self.theta_bcs_d ] # Adapt Neumann conditions. n = FacetNormal(self.Q.mesh()) self.theta_bcs_n = { k: dot(n, grad(theta_reference)) # k: Constant(1000.0) for k in self.theta_bcs_n } if DEBUG: # Solve the heat equation with the mixed Dirichlet-Neumann # boundary conditions and compare it to the Dirichlet-only # solution. theta_new = Function(self.Q, name="temperature (Neumann + Dirichlet)") from dolfin import Measure ds_workpiece = Measure("ds", subdomain_data=self.wp_boundaries) heat = cyl_heat.Heat( self.Q, convection=None, kappa=k, rho=rho, cp=cp, source=Constant(0.0), dirichlet_bcs=self.theta_bcs_d, neumann_bcs=self.theta_bcs_n, robin_bcs=self.theta_bcs_r, my_ds=ds_workpiece, ) theta_new = heat.solve_stationary() theta_new.rename("theta", "temperature (Neumann + Dirichlet)") from dolfin import plot, interactive, errornorm print("||theta_new - theta_ref|| = {:e}".format( errornorm(theta_new, theta_reference))) plot(theta_reference) plot(theta_new) plot(theta_reference - theta_new, title="theta_ref - theta_new") interactive() self.background_temp = 1400.0 # self.omega = 2 * pi * 10.0e3 self.omega = 2 * pi * 300.0 return
def __init__(self, PD, DD, verbosity): """ Constructor :param ProblemData PD: Problem information and various mesh-region <-> xs-material mappings :param Discretization DD: Discretization data :param int verbosity: Verbosity level. """ super(FluxModule, self).__init__() self.verb = verbosity self.print_prefix = "" self.mat_file_name = dict() self.PD = PD self.DD = DD self.BC = PD.bc try: self.fixed_source_problem = PD.fixed_source_problem self.eigenproblem = PD.eigenproblem except AttributeError: PD.distribute_material_data(DD.cell_regions, DD.M) self.A = PETScMatrix() # unused in case of an eigenvalue problem self.Q = PETScVector() # used only for saving the algebraic system self.rows_A = None self.cols_A = None self.vals_A = None self.rows_B = None self.cols_B = None self.vals_B = None if self.fixed_source_problem: self.fixed_source = Function(self.DD.V0) self.vals_Q = None # multigroup scalar fluxes self.phi_mg = [] for g in range(self.DD.G): phig = Function(self.DD.Vphi1) phig.rename("phi","phi_g{}".format(g)) self.phi_mg.append(phig) self.sln = Function(DD.V) self.sln_vec = as_backend_type(self.sln.vector()) self.local_sln_size = self.sln_vec.local_size() # auxiliary function for storing various DG(0) quantities (cross sections, group-integrated reaction rates, etc.) self.R = Function(self.DD.V0) # fission spectrum if 'chi' in self.PD.used_xs: self.chi = Function(self.DD.V0) else: self.chi = None if 'eSf' in self.PD.used_xs: self.E = numpy.zeros(self.DD.local_ndof0) else: self.E = None self.up_to_date = {"flux" : False, "cell_powers" : False} self.bnd_matrix_form = None self.parameters = parameters["flux_module"] if self.eigenproblem: assert self.parameters.has_parameter_set("eigensolver") self.eigen_params = self.parameters["eigensolver"] self.adaptive_eig_tol_end = 0 self.B = PETScMatrix() self.keff = 1 self.prev_keff = self.keff self.set_initial_approximation(numpy.random.random(self.local_sln_size)) self.update_phi() self.u = TrialFunction(self.DD.V) self.v = TestFunction(self.DD.V) self.v0 = TestFunction(self.DD.V0) self.phig = Function(self.DD.Vphi1) # single group scalar flux self.cell_RR_form = self.R * self.phig * self.v0 * dx self._cell_RRg_vector = PETScVector() self.vis_folder = os.path.join(self.PD.out_folder, "FLUX") self.vis_files = dict() var = "cell_powers" self.vis_files[var] = File(os.path.join(self.vis_folder, var+".pvd"), "compressed") var = "flux" self.vis_files[var] = [ File(os.path.join(self.vis_folder, "{}_g{}.pvd".format(var, g)), "compressed") for g in range(self.DD.G) ] variables = self.parameters["saving"].iterkeys() self.save_folder = { k : os.path.join(self.PD.out_folder, k.upper()) for k in variables }
class BlendedAlgebraicVofModel(VOFMixin, MultiPhaseModel): description = 'A blended algebraic VOF scheme implementing HRIC/CICSAM type schemes' def __init__(self, simulation): """ A blended algebraic VOF scheme works by using a specific convection scheme in the advection of the colour function that ensures a sharp interface. * The convection scheme should be the name of a convection scheme that is tailored for advection of the colour function, i.e "HRIC", "MHRIC", "RHRIC" etc, * The velocity field should be divergence free The colour function is unity when rho=rho0 and nu=nu0 and zero when rho=rho1 and nu=nu1 """ self.simulation = simulation simulation.log.info('Creating blended VOF multiphase model') # Define function space and solution function V = simulation.data['Vc'] self.degree = V.ufl_element().degree() simulation.data['c'] = Function(V) simulation.data['cp'] = Function(V) simulation.data['cpp'] = Function(V) # The projected density and viscosity functions for the new time step can be made continuous self.continuous_fields = simulation.input.get_value( 'multiphase_solver/continuous_fields', CONTINUOUS_FIELDS, 'bool') if self.continuous_fields: simulation.log.info(' Using continuous rho and nu fields') mesh = simulation.data['mesh'] V_cont = dolfin.FunctionSpace(mesh, 'CG', self.degree + 1) self.continuous_c = dolfin.Function(V_cont) self.continuous_c_old = dolfin.Function(V_cont) self.continuous_c_oldold = dolfin.Function(V_cont) self.force_bounded = simulation.input.get_value( 'multiphase_solver/force_bounded', FORCE_BOUNDED, 'bool') self.force_sharp = simulation.input.get_value( 'multiphase_solver/force_sharp', FORCE_SHARP, 'bool') # Calculate mu from rho and nu (i.e mu is quadratic in c) or directly from c (linear in c) self.calculate_mu_directly_from_colour_function = simulation.input.get_value( 'multiphase_solver/calculate_mu_directly_from_colour_function', CALCULATE_MU_DIRECTLY_FROM_COLOUR_FUNCTION, 'bool', ) # Get the physical properties self.set_physical_properties(read_input=True) # The convection blending function that counteracts numerical diffusion scheme = simulation.input.get_value('convection/c/convection_scheme', CONVECTION_SCHEME, 'string') simulation.log.info( ' Using convection scheme %s for the colour function' % scheme) scheme_class = get_convection_scheme(scheme) self.convection_scheme = scheme_class(simulation, 'c') self.need_gradient = scheme_class.need_alpha_gradient # Create the equations when the simulation starts simulation.hooks.add_pre_simulation_hook( self.on_simulation_start, 'BlendedAlgebraicVofModel setup equations') # Update the rho and nu fields before each time step simulation.hooks.add_pre_timestep_hook( self.update, 'BlendedAlgebraicVofModel - update colour field') simulation.hooks.register_custom_hook_point('MultiPhaseModelUpdated') # Linear solver # This causes the MPI unit tests to fail in "random" places for some reason # Quick fix: lazy loading of the solver LAZY_LOAD_SOLVER = True if LAZY_LOAD_SOLVER: self.solver = None else: self.solver = linear_solver_from_input( self.simulation, 'solver/c', default_parameters=SOLVER_OPTIONS) # Subcycle the VOF calculation multiple times per Navier-Stokes time step self.num_subcycles = scheme = simulation.input.get_value( 'multiphase_solver/num_subcycles', NUM_SUBCYCLES, 'int') if self.num_subcycles < 1: self.num_subcycles = 1 # Time stepping based on the subcycled values if self.num_subcycles == 1: self.cp = simulation.data['cp'] self.cpp = simulation.data['cpp'] else: self.cp = dolfin.Function(V) self.cpp = dolfin.Function(V) # Plot density and viscosity fields for visualization self.plot_fields = simulation.input.get_value( 'multiphase_solver/plot_fields', PLOT_FIELDS, 'bool') if self.plot_fields: V_plot = V if not self.continuous_fields else V_cont self.rho_for_plot = Function(V_plot) self.nu_for_plot = Function(V_plot) self.rho_for_plot.rename('rho', 'Density') self.nu_for_plot.rename('nu', 'Kinematic viscosity') simulation.io.add_extra_output_function(self.rho_for_plot) simulation.io.add_extra_output_function(self.nu_for_plot) # Slope limiter in case we are using DG1, not DG0 self.slope_limiter = SlopeLimiter(simulation, 'c', simulation.data['c']) simulation.log.info(' Using slope limiter: %s' % self.slope_limiter.limiter_method) self.is_first_timestep = True def on_simulation_start(self): """ This runs when the simulation starts. It does not run in __init__ since the solver needs the density and viscosity we define, and we need the velocity that is defined by the solver """ sim = self.simulation beta = self.convection_scheme.blending_function # The time step (real value to be supplied later) self.dt = Constant(sim.dt / self.num_subcycles) # Setup the equation to solve c = sim.data['c'] cp = self.cp cpp = self.cpp dirichlet_bcs = sim.data['dirichlet_bcs'].get('c', []) # Use backward Euler (BDF1) for timestep 1 self.time_coeffs = Constant([1, -1, 0]) if dolfin.norm(cpp.vector()) > 0 and self.num_subcycles == 1: # Use BDF2 from the start self.time_coeffs.assign(Constant([3 / 2, -2, 1 / 2])) sim.log.info( 'Using second order timestepping from the start in BlendedAlgebraicVOF' ) # Make sure the convection scheme has something useful in the first iteration c.assign(sim.data['cp']) if self.num_subcycles > 1: cp.assign(sim.data['cp']) # Plot density and viscosity self.update_plot_fields() # Define equation for advection of the colour function # ∂c/∂t + ∇⋅(c u) = 0 Vc = sim.data['Vc'] project_dgt0 = sim.input.get_value( 'multiphase_solver/project_uconv_dgt0', True, 'bool') if self.degree == 0 and project_dgt0: self.vel_dgt0_projector = VelocityDGT0Projector( sim, sim.data['u_conv']) self.u_conv = self.vel_dgt0_projector.velocity else: self.u_conv = sim.data['u_conv'] forcing_zones = sim.data['forcing_zones'].get('c', []) self.eq = AdvectionEquation( sim, Vc, cp, cpp, self.u_conv, beta, time_coeffs=self.time_coeffs, dirichlet_bcs=dirichlet_bcs, forcing_zones=forcing_zones, dt=self.dt, ) if self.need_gradient: # Reconstruct the gradient from the colour function DG0 field self.convection_scheme.initialize_gradient() # Notify listeners that the initial values are available sim.hooks.run_custom_hook('MultiPhaseModelUpdated') def get_colour_function(self, k): """ Return the colour function on timestep t^{n+k} """ if k == 0: if self.continuous_fields: c = self.continuous_c else: c = self.simulation.data['c'] elif k == -1: if self.continuous_fields: c = self.continuous_c_old else: c = self.simulation.data['cp'] elif k == -2: if self.continuous_fields: c = self.continuous_c_oldold else: c = self.simulation.data['cpp'] if self.force_bounded: c = dolfin.max_value(dolfin.min_value(c, Constant(1.0)), Constant(0.0)) if self.force_sharp: c = dolfin.conditional(dolfin.ge(c, 0.5), Constant(1.0), Constant(0.0)) return c def update_plot_fields(self): """ These fields are only needed to visualise the rho and nu fields in xdmf format for Paraview or similar """ if not self.plot_fields: return V = self.rho_for_plot.function_space() dolfin.project(self.get_density(0), V, function=self.rho_for_plot) dolfin.project(self.get_laminar_kinematic_viscosity(0), V, function=self.nu_for_plot) def update(self, timestep_number, t, dt): """ Update the VOF field by advecting it for a time dt using the given divergence free velocity field """ timer = dolfin.Timer('Ocellaris update VOF') sim = self.simulation # Get the functions c = sim.data['c'] cp = sim.data['cp'] cpp = sim.data['cpp'] # Stop early if the free surface is forced to stay still force_static = sim.input.get_value('multiphase_solver/force_static', FORCE_STATIC, 'bool') if force_static: c.assign(cp) cpp.assign(cp) timer.stop() # Stop timer before hook sim.hooks.run_custom_hook('MultiPhaseModelUpdated') self.is_first_timestep = False return if timestep_number != 1: # Update the previous values cpp.assign(cp) cp.assign(c) if self.degree == 0: self.vel_dgt0_projector.update() # Reconstruct the gradients if self.need_gradient: self.convection_scheme.gradient_reconstructor.reconstruct() # Update the convection blending factors is_static = isinstance(self.convection_scheme, StaticScheme) if not is_static: self.convection_scheme.update(dt / self.num_subcycles, self.u_conv) # Update global bounds in slope limiter if self.is_first_timestep: lo, hi = self.slope_limiter.set_global_bounds(lo=0.0, hi=1.0) if self.slope_limiter.has_global_bounds: sim.log.info( 'Setting global bounds [%r, %r] in BlendedAlgebraicVofModel' % (lo, hi)) # Solve the advection equations for the colour field if timestep_number == 1 or is_static: c.assign(cp) else: if self.solver is None: sim.log.info('Creating colour function solver', flush=True) self.solver = linear_solver_from_input( self.simulation, 'solver/c', default_parameters=SOLVER_OPTIONS) # Solve the advection equation A = self.eq.assemble_lhs() for _ in range(self.num_subcycles): b = self.eq.assemble_rhs() self.solver.inner_solve(A, c.vector(), b, 1, 0) self.slope_limiter.run() if self.num_subcycles > 1: self.cpp.assign(self.cp) self.cp.assign(c) # Optionally use a continuous predicted colour field if self.continuous_fields: Vcg = self.continuous_c.function_space() dolfin.project(c, Vcg, function=self.continuous_c) dolfin.project(cp, Vcg, function=self.continuous_c_old) dolfin.project(cpp, Vcg, function=self.continuous_c_oldold) # Report properties of the colour field sim.reporting.report_timestep_value('min(c)', c.vector().min()) sim.reporting.report_timestep_value('max(c)', c.vector().max()) # The next update should use the dt from this time step of the # main Navier-Stoke solver. The update just computed above uses # data from the previous Navier-Stokes solve with the previous dt self.dt.assign(dt / self.num_subcycles) if dt != sim.dt_prev: # Temporary switch to first order timestepping for the next # time step. This code is run before the Navier-Stokes solver # in each time step sim.log.info( 'VOF solver is first order this time step due to change in dt') self.time_coeffs.assign(Constant([1.0, -1.0, 0.0])) else: # Use second order backward time difference next time step self.time_coeffs.assign(Constant([3 / 2, -2.0, 1 / 2])) self.update_plot_fields() timer.stop() # Stop timer before hook sim.hooks.run_custom_hook('MultiPhaseModelUpdated') self.is_first_timestep = False
def test(show=False): problem = problems.Crucible() # The voltage is defined as # # v(t) = Im(exp(i omega t) v) # = Im(exp(i (omega t + arg(v)))) |v| # = sin(omega t + arg(v)) |v|. # # Hence, for a lagging voltage, arg(v) needs to be negative. voltages = [ 38.0 * numpy.exp(-1j * 2 * pi * 2 * 70.0 / 360.0), 38.0 * numpy.exp(-1j * 2 * pi * 1 * 70.0 / 360.0), 38.0 * numpy.exp(-1j * 2 * pi * 0 * 70.0 / 360.0), 25.0 * numpy.exp(-1j * 2 * pi * 0 * 70.0 / 360.0), 25.0 * numpy.exp(-1j * 2 * pi * 1 * 70.0 / 360.0), ] lorentz, joule, Phi = get_lorentz_joule(problem, voltages, show=show) # Some assertions ref = 1.4627674791126285e-05 assert abs(norm(Phi[0], "L2") - ref) < 1.0e-3 * ref ref = 3.161363929287592e-05 assert abs(norm(Phi[1], "L2") - ref) < 1.0e-3 * ref # ref = 12.115309575057681 assert abs(norm(lorentz, "L2") - ref) < 1.0e-3 * ref # ref = 1406.336109054347 V = FunctionSpace(problem.submesh_workpiece, "CG", 1) jp = project(joule, V) jp.rename("s", "Joule heat source") assert abs(norm(jp, "L2") - ref) < 1.0e-3 * ref # check_currents = False # if check_currents: # r = SpatialCoordinate(problem.mesh)[0] # begin('Currents computed after the fact:') # k = 0 # with XDMFFile('currents.xdmf') as xdmf_file: # for coil in coils: # for ii in coil['rings']: # J_r = sigma[ii] * ( # voltages[k].real/(2*pi*r) + problem.omega * Phi[1] # ) # J_i = sigma[ii] * ( # voltages[k].imag/(2*pi*r) - problem.omega * Phi[0] # ) # alpha = assemble(J_r * dx(ii)) # beta = assemble(J_i * dx(ii)) # info('J = {:e} + i {:e}'.format(alpha, beta)) # info( # '|J|/sqrt(2) = {:e}'.format( # numpy.sqrt(0.5 * (alpha**2 + beta**2)) # )) # submesh = SubMesh(problem.mesh, problem.subdomains, ii) # V1 = FunctionSpace(submesh, 'CG', 1) # # Those projections may take *very* long. # # TODO find out why # j_v1 = [ # project(J_r, V1), # project(J_i, V1) # ] # # show=Trueplot(j_v1[0], title='j_r') # # plot(j_v1[1], title='j_i') # current = project(as_vector(j_v1), V1*V1) # current.rename('j{}'.format(ii), 'current {}'.format(ii)) # xdmf_file.write(current) # k += 1 # end() filename = "./maxwell.xdmf" with XDMFFile(filename) as xdmf_file: xdmf_file.parameters["flush_output"] = True xdmf_file.parameters["rewrite_function_mesh"] = False # Store phi info("Writing out Phi to {}...".format(filename)) V = FunctionSpace(problem.mesh, "CG", 1) phi = Function(V, name="phi") Phi0 = project(Phi[0], V) Phi1 = project(Phi[1], V) omega = problem.omega for t in numpy.linspace(0.0, 2 * pi / omega, num=100, endpoint=False): # Im(Phi * exp(i*omega*t)) phi.vector().zero() phi.vector().axpy(sin(problem.omega * t), Phi0.vector()) phi.vector().axpy(cos(problem.omega * t), Phi1.vector()) xdmf_file.write(phi, t) # Show the resulting magnetic field # # B_r = -dphi/dz, # B_z = 1/r d(rphi)/dr. # r = SpatialCoordinate(problem.mesh)[0] g = 1.0 / r * grad(r * Phi[0]) V_element = FiniteElement("CG", V.mesh().ufl_cell(), 1) VV = FunctionSpace(V.mesh(), V_element * V_element) B_r = project(as_vector((-g[1], g[0])), VV) g = 1 / r * grad(r * Phi[1]) B_i = project(as_vector((-g[1], g[0])), VV) info("Writing out B to {}...".format(filename)) B = Function(VV) B.rename("B", "magnetic field") if abs(problem.omega) < DOLFIN_EPS: B.assign(B_r) xdmf_file.write(B) # plot(B_r, title='Re(B)') # plot(B_i, title='Im(B)') else: # Write those out to a file. lspace = numpy.linspace( 0.0, 2 * pi / problem.omega, num=100, endpoint=False ) for t in lspace: # Im(B * exp(i*omega*t)) B.vector().zero() B.vector().axpy(sin(problem.omega * t), B_r.vector()) B.vector().axpy(cos(problem.omega * t), B_i.vector()) xdmf_file.write(B, t) filename = "./lorentz-joule.xdmf" info("Writing out Lorentz force and Joule heat source to {}...".format(filename)) with XDMFFile(filename) as xdmf_file: xdmf_file.write(lorentz, 0.0) # xdmf_file.write(jp, 0.0) return
def test(show=False): problem = problems.Crucible() # The voltage is defined as # # v(t) = Im(exp(i omega t) v) # = Im(exp(i (omega t + arg(v)))) |v| # = sin(omega t + arg(v)) |v|. # # Hence, for a lagging voltage, arg(v) needs to be negative. voltages = [ 38.0 * numpy.exp(-1j * 2 * pi * 2 * 70.0 / 360.0), 38.0 * numpy.exp(-1j * 2 * pi * 1 * 70.0 / 360.0), 38.0 * numpy.exp(-1j * 2 * pi * 0 * 70.0 / 360.0), 25.0 * numpy.exp(-1j * 2 * pi * 0 * 70.0 / 360.0), 25.0 * numpy.exp(-1j * 2 * pi * 1 * 70.0 / 360.0), ] lorentz, joule, Phi = get_lorentz_joule(problem, voltages, show=show) # Some assertions ref = 1.4627674791126285e-05 assert abs(norm(Phi[0], "L2") - ref) < 1.0e-3 * ref ref = 3.161363929287592e-05 assert abs(norm(Phi[1], "L2") - ref) < 1.0e-3 * ref # ref = 12.115309575057681 assert abs(norm(lorentz, "L2") - ref) < 1.0e-3 * ref # ref = 1406.336109054347 V = FunctionSpace(problem.submesh_workpiece, "CG", 1) jp = project(joule, V) jp.rename("s", "Joule heat source") assert abs(norm(jp, "L2") - ref) < 1.0e-3 * ref # check_currents = False # if check_currents: # r = SpatialCoordinate(problem.mesh)[0] # begin('Currents computed after the fact:') # k = 0 # with XDMFFile('currents.xdmf') as xdmf_file: # for coil in coils: # for ii in coil['rings']: # J_r = sigma[ii] * ( # voltages[k].real/(2*pi*r) + problem.omega * Phi[1] # ) # J_i = sigma[ii] * ( # voltages[k].imag/(2*pi*r) - problem.omega * Phi[0] # ) # alpha = assemble(J_r * dx(ii)) # beta = assemble(J_i * dx(ii)) # info('J = {:e} + i {:e}'.format(alpha, beta)) # info( # '|J|/sqrt(2) = {:e}'.format( # numpy.sqrt(0.5 * (alpha**2 + beta**2)) # )) # submesh = SubMesh(problem.mesh, problem.subdomains, ii) # V1 = FunctionSpace(submesh, 'CG', 1) # # Those projections may take *very* long. # # TODO find out why # j_v1 = [ # project(J_r, V1), # project(J_i, V1) # ] # # show=Trueplot(j_v1[0], title='j_r') # # plot(j_v1[1], title='j_i') # current = project(as_vector(j_v1), V1*V1) # current.rename('j{}'.format(ii), 'current {}'.format(ii)) # xdmf_file.write(current) # k += 1 # end() filename = "./maxwell.xdmf" with XDMFFile(filename) as xdmf_file: xdmf_file.parameters["flush_output"] = True xdmf_file.parameters["rewrite_function_mesh"] = False # Store phi info("Writing out Phi to {}...".format(filename)) V = FunctionSpace(problem.mesh, "CG", 1) phi = Function(V, name="phi") Phi0 = project(Phi[0], V) Phi1 = project(Phi[1], V) omega = problem.omega for t in numpy.linspace(0.0, 2 * pi / omega, num=100, endpoint=False): # Im(Phi * exp(i*omega*t)) phi.vector().zero() phi.vector().axpy(sin(problem.omega * t), Phi0.vector()) phi.vector().axpy(cos(problem.omega * t), Phi1.vector()) xdmf_file.write(phi, t) # Show the resulting magnetic field # # B_r = -dphi/dz, # B_z = 1/r d(rphi)/dr. # r = SpatialCoordinate(problem.mesh)[0] g = 1.0 / r * grad(r * Phi[0]) V_element = FiniteElement("CG", V.mesh().ufl_cell(), 1) VV = FunctionSpace(V.mesh(), V_element * V_element) B_r = project(as_vector((-g[1], g[0])), VV) g = 1 / r * grad(r * Phi[1]) B_i = project(as_vector((-g[1], g[0])), VV) info("Writing out B to {}...".format(filename)) B = Function(VV) B.rename("B", "magnetic field") if abs(problem.omega) < DOLFIN_EPS: B.assign(B_r) xdmf_file.write(B) # plot(B_r, title='Re(B)') # plot(B_i, title='Im(B)') else: # Write those out to a file. lspace = numpy.linspace(0.0, 2 * pi / problem.omega, num=100, endpoint=False) for t in lspace: # Im(B * exp(i*omega*t)) B.vector().zero() B.vector().axpy(sin(problem.omega * t), B_r.vector()) B.vector().axpy(cos(problem.omega * t), B_i.vector()) xdmf_file.write(B, t) filename = "./lorentz-joule.xdmf" info("Writing out Lorentz force and Joule heat source to {}...".format( filename)) with XDMFFile(filename) as xdmf_file: xdmf_file.write(lorentz, 0.0) # xdmf_file.write(jp, 0.0) return