def rms_height_from_area(topography): """ Compute the root mean square height amplitude of a topography or line scan stored on a uniform grid from the whole areal data. (This is the Sq value.) Parameters ---------- topography : :obj:`SurfaceTopography` or :obj:`UniformLineScan` SurfaceTopography object containing height information. Returns ------- rms_height : float Root mean square height value. """ if topography.dim <= 1: raise ValueError( 'Areal rms height can only be computed for topographies, not line scans.' ) elif topography.dim == 2: n = np.prod(topography.nb_grid_pts) pnp = Reduction(topography._communicator) profile = topography.heights() return np.sqrt(pnp.sum((profile - pnp.sum(profile) / n)**2) / n) else: raise ValueError( f'Cannot handle topographies of dimension {topography.dim}')
def rms_height_from_area(topography): """ Compute the root mean square height amplitude of a topography or line scan stored on a uniform grid from the whole areal data. (This is the Sq value.) Parameters ---------- topography : :obj:`SurfaceTopography` or :obj:`UniformLineScan` SurfaceTopography object containing height information. Returns ------- rms_height : float Root mean square height value. """ n = np.prod(topography.nb_grid_pts) pnp = Reduction(topography._communicator) profile = topography.heights() return np.sqrt(pnp.sum((profile - pnp.sum(profile) / n)**2) / n)
def rms_height(topography, kind='Sq'): """ Compute the root mean square height amplitude of a topography or line scan stored on a uniform grid. Parameters ---------- topography : :obj:`SurfaceTopography` or :obj:`UniformLineScan` SurfaceTopography object containing height information. Returns ------- rms_height : float Root mean square height value. """ n = np.prod(topography.nb_grid_pts) # if topography.is_MPI: pnp = Reduction(topography._communicator) profile = topography.heights() if kind == 'Sq': return np.sqrt(pnp.sum((profile - pnp.sum(profile) / n)**2) / n) elif kind == 'Rq': # Problem: when one of the processors holds the full data he isn't able # to detect if any axis is MPI_Parallelized # this problem is solved automatically if we do not support one axis # to be zero decomp_axis = [ full != loc for full, loc in zip(np.array(topography.nb_grid_pts), profile.shape) ] temppnp = pnp if decomp_axis[0] else np return np.sqrt( temppnp.sum((profile - temppnp.sum(profile, axis=0) / topography.nb_grid_pts[0])**2) / n) else: raise RuntimeError("Unknown rms height kind '{}'.".format(kind))
def constrained_conjugate_gradients(substrate, topography, hardness=None, external_force=None, offset=None, initial_displacements=None, initial_forces=None, pentol=None, prestol=1e-5, mixfac=0.1, maxiter=100000, logger=None, callback=None, verbose=False): """ Use a constrained conjugate gradient optimization to find the equilibrium configuration deflection of an elastic manifold. The conjugate gradient iteration is reset using the steepest descent direction whenever the contact area changes. Method is described in I.A. Polonsky, L.M. Keer, Wear 231, 206 (1999) Parameters ---------- substrate : elastic manifold Elastic manifold. topography: SurfaceTopography object Height profile of the rigid counterbody hardness : array_like Hardness of the substrate. Pressure cannot exceed this value. Can be scalar or array (i.e. per pixel) value. external_force : float External force. Constrains the sum of forces to this value. offset : float Offset of rigid surface. Ignore if external_force is specified. initial_displacements : array_like Displacement field for initializing the solver. Guess an initial value if set to None. initial_forces: array_like pixel forces field for initializing the solver. Is computed from initial_displacements if none pentol : float Maximum penetration of contacting regions required for convergence. prestol : float maximum pressure outside the contact region allowed for convergence maxiter : float Maximum number of iterations. logger: ContactMechanics.Tools.Logger reports status and values at each iteration callback: callable(int iteration, array_link forces, dict d) called each iteration. The dictionary contains additional scalars verbose: bool If True, more scalar quantities are passed to the logger Returns ------- Optimisation result x: displacements fun: elastic energy jac: forces active_set: points where forces are not constrained to 0 or hardness offset: offset i rigid surface, results from the optimization processes when the external_force is constrained """ if substrate.nb_subdomain_grid_pts != substrate.nb_domain_grid_pts: # check that a topography instance is provided and not only a numpy # array if not hasattr(topography, "nb_grid_pts"): raise ValueError("You should provide a topography object when " "working with MPI") reduction = Reduction(substrate.communicator) # surface is the array holding the data assigned to the processsor if not hasattr(topography, "nb_grid_pts"): surface = topography topography = Topography(surface, physical_sizes=substrate.physical_sizes) else: surface = topography.heights() # Local data # Note: Suffix _r deontes real-space _q reciprocal space 2d-arrays nb_surface_pts = np.prod(topography.nb_grid_pts) if pentol is None: # Heuristics for the possible tolerance on penetration. # This is necessary because numbers can vary greatly # depending on the system of units. pentol = topography.rms_height_from_area() / ( 10 * np.mean(topography.nb_grid_pts)) # If pentol is zero, then this is a flat surface. This only makes # sense for nonperiodic calculations, i.e. it is a punch. Then # use the offset to determine the tolerance if pentol == 0: pentol = (offset + reduction.sum(surface[...]) / nb_surface_pts) \ / 1000 # If we are still zero use an arbitrary value if pentol == 0: pentol = 1e-3 surf_mask = np.ma.getmask( surface) # TODO: Test behaviour with masked arrays. if logger is not None: logger.pr('maxiter = {0}'.format(maxiter)) logger.pr('pentol = {0}'.format(pentol)) if offset is None: offset = 0 if initial_displacements is None: u_r = np.zeros(substrate.nb_subdomain_grid_pts) else: u_r = initial_displacements.copy() # slice of the local data of the computation subdomain corresponding to the # topography subdomain. It's typically the first half of the computation # subdomain (along the non-parallelized dimension) for FreeFFTElHS # It's the same for PeriodicFFTElHS comp_slice = [slice(0, max(0, min( substrate.nb_grid_pts[i] - substrate.subdomain_locations[i], substrate.nb_subdomain_grid_pts[i]))) for i in range(substrate.dim)] if substrate.dim not in (1, 2): raise Exception( ("Constrained conjugate gradient currently only implemented for 1 " "or 2 dimensions (Your substrate has {}.).").format( substrate.dim)) comp_mask = np.zeros(substrate.nb_subdomain_grid_pts, dtype=bool) comp_mask[tuple(comp_slice)] = True surf_mask = np.ma.getmask(surface) if surf_mask is np.ma.nomask: surf_mask = np.ones(topography.nb_subdomain_grid_pts, dtype=bool) else: comp_mask[tuple(comp_slice)][surf_mask] = False surf_mask = np.logical_not(surf_mask) pad_mask = np.logical_not(comp_mask) N_pad = reduction.sum(pad_mask * 1) u_r[comp_mask] = np.where(u_r[comp_mask] < surface[surf_mask] + offset, surface[surf_mask] + offset, u_r[comp_mask]) result = optim.OptimizeResult() result.nfev = 0 result.nit = 0 result.success = False result.message = "Not Converged (yet)" # Compute forces # p_r = -np.fft.ifft2(np.fft.fft2(u_r)/gf_q).real if initial_forces is None: p_r = substrate.evaluate_force(u_r) else: p_r = initial_forces.copy() u_r = substrate.evaluate_disp(p_r) result.nfev += 1 # Pressure outside the computational region must be zero p_r[pad_mask] = 0.0 # iteration delta = 0 delta_str = 'reset' G_old = 1.0 t_r = np.zeros_like(u_r) tau = 0.0 for it in range(1, maxiter + 1): result.nit = it # Reset contact area (area that feels compressive stress) c_r = p_r < 0.0 # TODO: maybe np.where(self.interaction.force < 0., 1., 0.) # Compute total contact area (area with compressive pressure) A_contact = reduction.sum(c_r * 1) # If a hardness is specified, exclude values that exceed the hardness # from the "contact area". Note: "contact area" here is the region that # is optimized by the CG iteration. if hardness is not None: c_r = np.logical_and(c_r, p_r > -hardness) # Compute total are treated by the CG optimizer (which exclude flowing) # portions. A_cg = reduction.sum(c_r * 1) # Compute gap g_r = u_r[comp_mask] - surface[surf_mask] if external_force is not None: offset = 0 if A_cg > 0: offset = reduction.sum(g_r[c_r[comp_mask]]) / A_cg g_r -= offset # Compute G = sum(g*g) (over contact area only) G = reduction.sum(c_r[comp_mask] * g_r * g_r) if delta_str != 'mix' and not (hardness is not None and A_cg == 0): # t = (g + delta*(G/G_old)*t) inside contact area and 0 outside if delta > 0 and G_old > 0: t_r[comp_mask] = c_r[comp_mask] * ( g_r + delta * (G / G_old) * t_r[comp_mask]) else: t_r[comp_mask] = c_r[comp_mask] * g_r # Compute elastic displacement that belong to t_r # substrate (Nelastic manifold: r_r is negative of Polonsky, # Kerr's r) # r_r = -np.fft.ifft2(gf_q*np.fft.fft2(t_r)).real r_r = substrate.evaluate_disp(t_r) result.nfev += 1 # Note: Sign reversed from Polonsky, Keer because this r_r is # negative of theirs. tau = 0.0 if A_cg > 0: # tau = -sum(g*t)/sum(r*t) where sum is only over contact # region x = -reduction.sum(c_r * r_r * t_r) if x > 0.0: tau = \ reduction.sum(c_r[comp_mask] * g_r * t_r[comp_mask]) \ / x else: G = 0.0 p_r += tau * c_r * t_r else: # The CG area can vanish if this is a plastic calculation. In that # case we need to use the gap to decide which regions contact. All # contact area should then be the hardness value. We use simple # relaxation algorithm to converge the contact area in that case. if delta_str != 'mixconv': delta_str = 'mix' # Mix pressure # p_r[comp_mask] = (1-mixfac)*p_r[comp_mask] + \ # mixfac*np.where(g_r < 0.0, # -hardness*np.ones_like(g_r), # np.zeros_like(g_r)) # Evolve pressure in direction of energy gradient # p_r[comp_mask] += mixfac*(u_r[comp_mask] + g_r) p_r[comp_mask] = (1 - mixfac) * p_r[ comp_mask] - mixfac * hardness * (g_r < 0.0) mixfac *= 0.5 # p_r[comp_mask] = -hardness*(g_r < 0.0) # Find area with tensile stress and negative gap # (i.e. penetration of the two surfaces) mask_tensile = p_r >= 0.0 nc_r = np.logical_and(mask_tensile[comp_mask], g_r < 0.0) # If hardness is specified, find area where pressure exceeds hardness # but gap is positive if hardness is not None: mask_flowing = p_r <= -hardness nc_r = np.logical_or(nc_r, np.logical_and(mask_flowing[comp_mask], g_r > 0.0)) # For nonperiodic calculations: Find maximum pressure in pad region. # This must be zero. pad_pres = 0 if N_pad > 0: pad_pres = reduction.max(abs(p_r[pad_mask])) # Find maximum pressure outside contacting region and the deviation # from hardness inside the flowing regions. This should go to zero. max_pres = 0 if reduction.sum(mask_tensile * 1) > 0: max_pres = reduction.max(p_r[mask_tensile] * 1) if hardness: A_fl = reduction.sum(mask_flowing) if A_fl > 0: max_pres = max(max_pres, -reduction.min(p_r[mask_flowing] + hardness)) # Set all tensile stresses to zero p_r[mask_tensile] = 0.0 # Adjust pressure if external_force is not None: psum = -reduction.sum(p_r[comp_mask]) if psum != 0: p_r *= external_force / psum else: p_r = -external_force / nb_surface_pts * np.ones_like(p_r) p_r[pad_mask] = 0.0 # If hardness is specified, set all stress larger than hardness to the # hardness value (i.e. truncate pressure) if hardness is not None: p_r[mask_flowing] = -hardness if delta_str != 'mix': if reduction.sum(nc_r * 1) > 0: # The contact area has changed! nc_r contains area that # penetrate but have zero (or tensile) pressure. They hence # violate the contact constraint. Update their forces and # reset the CG iteration. p_r[comp_mask] += tau * nc_r * g_r delta = 0 delta_str = 'sd' else: delta = 1 delta_str = 'cg' # Check convergence respective pressure converged = True psum = -reduction.sum(p_r[comp_mask]) if external_force is not None: converged = abs(psum - external_force) < prestol # Compute new displacements from updated forces # u_r = -np.fft.ifft2(gf_q*np.fft.fft2(p_r)).real new_u_r = substrate.evaluate_disp(p_r) maxdu = reduction.max(abs(new_u_r - u_r)) u_r = new_u_r result.nfev += 1 # Store G for next step G_old = G # Compute root-mean square penetration, max penetration and max force # difference between the steps if A_cg > 0: rms_pen = sqrt(G / A_cg) else: rms_pen = sqrt(G) max_pen = max(0.0, reduction.max(c_r[comp_mask] * (surface[surf_mask] + offset - u_r[comp_mask]))) result.maxcv = {"max_pen": max_pen, "max_pres": max_pres} # Elastic energy would be # e_el = -0.5*reduction.sum(p_r*u_r) if delta_str == 'mix': converged = converged and maxdu < pentol and \ max_pres < prestol and pad_pres < prestol else: converged = converged and rms_pen < pentol and \ max_pen < pentol and maxdu < pentol and \ max_pres < prestol and pad_pres < prestol log_headers = ['status', 'it', 'area', 'frac. area', 'total force', 'offset'] log_values = [delta_str, it, A_contact, A_contact / reduction.sum(surf_mask * 1), psum, offset] if hardness: log_headers += ['plast. area', 'frac.plast. area'] log_values += [A_fl, A_fl / reduction.sum(surf_mask * 1)] if verbose: log_headers += ['rms pen.', 'max. pen.', 'max. force', 'max. pad force', 'max. du', 'CG area', 'frac. CG area', 'sum(nc_r)'] log_values += [rms_pen, max_pen, max_pres, pad_pres, maxdu, A_cg, A_cg / reduction.sum(surf_mask * 1), reduction.sum(nc_r * 1)] if delta_str == 'mix': log_headers += ['mixfac'] log_values += [mixfac] else: log_headers += ['tau'] log_values += [tau] if converged and delta_str == 'mix': delta_str = 'mixconv' log_values[0] = delta_str mixfac = 0.5 elif converged: if logger is not None: log_values[0] = 'CONVERGED' logger.st(log_headers, log_values, force_print=True) # Return full u_r because this is required to reproduce pressure # from evalualte_force result.x = u_r # [comp_mask] # Return partial p_r because pressure outside computational region # is zero anyway result.jac = -p_r[tuple(comp_slice)] result.active_set = c_r # Compute elastic energy result.fun = -reduction.sum( p_r[tuple(comp_slice)] * u_r[tuple(comp_slice)]) / 2 result.offset = offset result.success = True result.message = "Polonsky converged" return result if logger is not None and it < maxiter: logger.st(log_headers, log_values) if callback is not None: d = dict(area=np.int64(A_contact).item(), fractional_area=np.float64( A_contact / reduction.sum(surf_mask)).item(), rms_penetration=np.float64(rms_pen).item(), max_penetration=np.float64(max_pen).item(), max_pressure=np.float64(max_pres).item(), pad_pressure=np.float64(pad_pres).item(), penetration_tol=np.float64(pentol).item(), pressure_tol=np.float64(prestol).item()) callback(it, p_r, d) if isnan(G) or isnan(rms_pen): raise RuntimeError('nan encountered.') if logger is not None: log_values[0] = 'NOT CONVERGED' logger.st(log_headers, log_values, force_print=True) # Return full u_r because this is required to reproduce pressure # from evalualte_force result.x = u_r # [comp_mask] # Return partial p_r because pressure outside computational region # is zero anyway result.jac = -p_r[tuple(comp_slice)] result.active_set = c_r # Compute elastic energy result.fun = -reduction.sum( (p_r[tuple(comp_slice)] * u_r[tuple(comp_slice)])) / 2 result.offset = offset result.message = "Reached maxiter = {}".format(maxiter) return result
def test_z_continue_another_test(comm): # however rank 0 only fails on this test. pnp = Reduction(comm) pnp.sum(np.array([3, 4])) assert True
class PeriodicFFTElasticHalfSpace(ElasticSubstrate): """ Uses the FFT to solve the displacements and stresses in an elastic Halfspace due to a given array of point forces. This halfspace implementation cheats somewhat: since a net pressure would result in infinite displacement, the first term of the FFT is systematically dropped. The implementation follows the description in Stanley & Kato J. Tribol. 119(3), 481-485 (Jul 01, 1997) """ name = "periodic_fft_elastic_halfspace" _periodic = True def __init__(self, nb_grid_pts, young, physical_sizes=2 * np.pi, stiffness_q0=None, thickness=None, poisson=0.0, superclass=True, fft="serial", communicator=None): """ Parameters ---------- nb_grid_pts : int tuple containing number of points in spatial directions. The length of the tuple determines the spatial dimension of the problem. young : float Young's modulus, if poisson is not specified it is the contact modulus as defined in Johnson, Contact Mechanics physical_sizes : float or float tuple (default 2π) domain size. For multidimensional problems, a tuple can be provided to specify the lengths per dimension. If the tuple has less entries than dimensions, the last value in repeated. stiffness_q0 : float, optional Substrate stiffness at the Gamma-point (wavevector q=0). If None, this is taken equal to the lowest nonvanishing stiffness. Cannot be used in combination with thickness. thickness : float, optional Thickness of the elastic half-space. If None, this models an infinitely deep half-space. Cannot be used in combination with stiffness_q0. poisson : float Default 0 Poisson number. Need only be specified for substrates of finite thickness. If left unspecified for substrates of infinite thickness, then young is the contact modulus. superclass : bool (default True) client software never uses this. Only inheriting subclasses use this. fft: string Default: 'serial' FFT engine to use. Options are 'fftw', 'fftwmpi', 'pfft' and 'p3dfft'. 'serial' and 'mpi' can also be specified, where the choice of the appropriate fft is made by muFFT communicator : mpi4py communicator or NuMPI stub communicator MPI communicator object. """ super().__init__() if not hasattr(nb_grid_pts, "__iter__"): nb_grid_pts = (nb_grid_pts, ) if not hasattr(physical_sizes, "__iter__"): physical_sizes = (physical_sizes, ) self.__dim = len(nb_grid_pts) if self.dim not in (1, 2): raise self.Error( ("Dimension of this problem is {}. Only 1 and 2-dimensional " "problems are supported").format(self.dim)) if stiffness_q0 is not None and thickness is not None: raise self.Error("Please specify either stiffness_q0 or thickness " "or neither.") self._nb_grid_pts = nb_grid_pts tmpsize = list() for i in range(self.dim): tmpsize.append(physical_sizes[min(i, len(physical_sizes) - 1)]) self._physical_sizes = tuple(tmpsize) try: self._steps = tuple( float(size) / res for size, res in zip(self.physical_sizes, self.nb_grid_pts)) except ZeroDivisionError as err: raise ZeroDivisionError( ("{}, when trying to handle " " self._steps = tuple(" " float(physical_sizes)/res for physical_sizes, res in" " zip(self.physical_sizes, self.nb_grid_pts))" "Parameters: self.physical_sizes = {}, self.nb_grid_pts = {}" "").format(err, self.physical_sizes, self.nb_grid_pts)) self.young = young self.poisson = poisson self.contact_modulus = young / (1 - poisson**2) self.stiffness_q0 = stiffness_q0 self.thickness = thickness self.fftengine = FFT(self.nb_domain_grid_pts, fft=fft, communicator=communicator, allow_temporary_buffer=False, allow_destroy_input=True) # Allocate buffers and create plan for one degree of freedom self.real_buffer = self.fftengine.register_real_space_field( "real-space", 1) self.fourier_buffer = self.fftengine.register_fourier_space_field( "fourier-space", 1) self.greens_function = None self.surface_stiffness = None self._communicator = communicator self.pnp = Reduction(communicator) if superclass: self.greens_function = self._compute_greens_function() self.surface_stiffness = self._compute_surface_stiffness() @property def dim(self, ): "return the substrate's physical dimension" return self.__dim @property def nb_grid_pts(self): return self._nb_grid_pts @property def area_per_pt(self): return np.prod(self.physical_sizes) / np.prod(self.nb_grid_pts) @property def physical_sizes(self): return self._physical_sizes @property def nb_domain_grid_pts(self, ): """ usually, the nb_grid_pts of the system is equal to the geometric nb_grid_pts (of the surface). For example free boundary conditions, require the computational nb_grid_pts to differ from the geometric one, see FreeFFTElasticHalfSpace. """ return self.nb_grid_pts @property def nb_subdomain_grid_pts(self): """ When working in Parallel one processor holds only Part of the Data :return: """ return self.fftengine.nb_subdomain_grid_pts @property def topography_nb_subdomain_grid_pts(self): return self.nb_subdomain_grid_pts @property def subdomain_locations(self): """ When working in Parallel one processor holds only Part of the Data :return: """ return self.fftengine.subdomain_locations @property def topography_subdomain_locations(self): return self.subdomain_locations @property def subdomain_slices(self): """ When working in Parallel one processor holds only Part of the Data :return: """ return self.fftengine.subdomain_slices @property def topography_subdomain_slices(self): return tuple([ slice(s, s + n) for s, n in zip(self.topography_subdomain_locations, self.topography_nb_subdomain_grid_pts) ]) @property def local_topography_subdomain_slices(self): """ slice representing the local subdomain without the padding area """ return tuple( [slice(0, n) for n in self.topography_nb_subdomain_grid_pts]) @property def nb_fourier_grid_pts(self): """ When working in Parallel one processor holds only Part of the Data :return: """ return self.fftengine.nb_fourier_grid_pts @property def fourier_locations(self): """ When working in Parallel one processor holds only Part of the Data :return: """ return self.fftengine.fourier_locations @property def fourier_slices(self): """ When working in Parallel one processor holds only Part of the Data :return: """ return self.fftengine.fourier_slices @property def communicator(self): """Return the MPI communicator""" return self._communicator def __repr__(self): dims = 'x', 'y', 'z' size_str = ', '.join('{}: {}({})'.format(dim, size, nb_grid_pts) for dim, size, nb_grid_pts in zip( dims, self.physical_sizes, self.nb_grid_pts)) return "{0.dim}-dimensional halfspace '{0.name}', " \ "physical_sizes(nb_grid_pts) in {1}, E' = {0.young}" \ .format(self, size_str) def _compute_greens_function(self): r""" Compute the weights w relating fft(displacement) to fft(pressure): fft(u) = w*fft(p), see (6) Stanley & Kato J. Tribol. 119(3), 481-485 (Jul 01, 1997). For the infinite halfspace, .. math :: w = q E^* / 2 q is the wavevector (:math:`2 \pi / wavelength`) WARNING: the paper is dimensionally *incorrect*. see for the correct 1D formulation: Section 13.2 in K. L. Johnson. (1985). Contact Mechanics. [Online]. Cambridge: Cambridge University Press. Available from: Cambridge Books Online <http://dx.doi.org/10.1017/CBO9781139171731> [Accessed 16 February 2015] for correct 2D formulation: Appendix 1, eq A.2 in Johnson, Greenwood and Higginson, "The Contact of Elastic Regular Wavy surfaces", Int. J. Mech. Sci. Vol. 27 No. 6, pp. 383-396, 1985 <http://dx.doi.org/10.1016/0020-7403(85)90029-3> [Accessed 18 March 2015] """ if self.dim == 1: nx, = self.nb_grid_pts sx, = self.physical_sizes # Note: q-values from 0 to 1, not from 0 to 2*pi qx = np.arange(self.fourier_locations[0], self.fourier_locations[0] + self.nb_fourier_grid_pts[0], dtype=np.float64) qx = np.where(qx <= nx // 2, qx / sx, (nx - qx) / sx) surface_stiffness = np.pi * self.contact_modulus * qx if self.stiffness_q0 is None: surface_stiffness[0] = surface_stiffness[1].real elif self.stiffness_q0 == 0.0: surface_stiffness[0] = 1.0 else: surface_stiffness[0] = self.stiffness_q0 greens_function = 1 / surface_stiffness if self.fourier_locations == (0, ): if self.stiffness_q0 == 0.0: greens_function[0, 0] = 0.0 elif self.dim == 2: if np.prod(self.nb_fourier_grid_pts) == 0: greens_function = np.zeros(self.nb_fourier_grid_pts, order='f', dtype=complex) else: nx, ny = self.nb_grid_pts sx, sy = self.physical_sizes # Note: q-values from 0 to 1, not from 0 to 2*pi qx = np.arange(self.fourier_locations[0], self.fourier_locations[0] + self.nb_fourier_grid_pts[0], dtype=np.float64) qx = np.where(qx <= nx // 2, qx / sx, (nx - qx) / sx) qy = np.arange(self.fourier_locations[1], self.fourier_locations[1] + self.nb_fourier_grid_pts[1], dtype=np.float64) qy = np.where(qy <= ny // 2, qy / sy, (ny - qy) / sy) q = np.sqrt((qx * qx).reshape(-1, 1) + (qy * qy).reshape(1, -1)) if self.fourier_locations == (0, 0): q[0, 0] = np.NaN # q[0,0] has no Impact on the end result, # but q[0,0] = 0 produces runtime Warnings # (because corr[0,0]=inf) surface_stiffness = np.pi * self.contact_modulus * q # E* / 2 (2 \pi / \lambda) # (q is 1 / lambda, here) if self.thickness is not None: # Compute correction for finite thickness q *= 2 * np.pi * self.thickness fac = 3 - 4 * self.poisson off = 4 * self.poisson * (2 * self.poisson - 3) + 5 with np.errstate(over="ignore", invalid="ignore", divide="ignore"): corr = (fac * np.cosh(2 * q) + 2 * q ** 2 + off) / \ (fac * np.sinh(2 * q) - 2 * q) # The expression easily overflows numerically. These are # then q-values that are converged to the infinite system # expression. corr[np.isnan(corr)] = 1.0 surface_stiffness *= corr if self.fourier_locations == (0, 0): surface_stiffness[0, 0] = \ self.young / self.thickness * \ (1 - self.poisson) / ((1 - 2 * self.poisson) * (1 + self.poisson)) else: if self.fourier_locations == (0, 0): if self.stiffness_q0 is None: surface_stiffness[0, 0] = \ (surface_stiffness[1, 0].real + surface_stiffness[0, 1].real) / 2 elif self.stiffness_q0 == 0.0: surface_stiffness[0, 0] = 1.0 else: surface_stiffness[0, 0] = self.stiffness_q0 greens_function = 1 / surface_stiffness if self.fourier_locations == (0, 0): if self.stiffness_q0 == 0.0: greens_function[0, 0] = 0.0 return greens_function def _compute_surface_stiffness(self): """ Invert the weights w relating fft(displacement) to fft(pressure): """ surface_stiffness = np.zeros(self.nb_fourier_grid_pts, order='f', dtype=complex) surface_stiffness[self.greens_function != 0] = \ 1. / self.greens_function[self.greens_function != 0] return surface_stiffness def evaluate_disp(self, forces): """ Computes the displacement due to a given force array Keyword Arguments: forces -- a numpy array containing point forces (*not* pressures) """ if forces.shape != self.nb_subdomain_grid_pts: raise self.Error( ("force array has a different shape ({0}) than this " "halfspace's nb_grid_pts ({1})").format( forces.shape, self.nb_subdomain_grid_pts)) self.real_buffer.array()[...] = -forces self.fftengine.fft(self.real_buffer, self.fourier_buffer) self.fourier_buffer.array()[...] *= self.greens_function self.fftengine.ifft(self.fourier_buffer, self.real_buffer) return self.real_buffer.array().real / \ self.area_per_pt * self.fftengine.normalisation def evaluate_force(self, disp): """ Computes the force (*not* pressures) due to a given displacement array. Keyword Arguments: disp -- a numpy array containing point displacements """ if disp.shape != self.nb_subdomain_grid_pts: raise self.Error( ("displacements array has a different shape ({0}) than " "this halfspace's nb_grid_pts ({1})").format( disp.shape, self.nb_subdomain_grid_pts)) self.real_buffer.array()[...] = disp self.fftengine.fft(self.real_buffer, self.fourier_buffer) self.fourier_buffer.array()[...] *= self.surface_stiffness self.fftengine.ifft(self.fourier_buffer, self.real_buffer) return -self.real_buffer.array().real * \ self.area_per_pt * self.fftengine.normalisation def evaluate_k_disp(self, forces): """ Computes the K-space displacement due to a given force array Keyword Arguments: forces -- a numpy array containing point forces (*not* pressures) """ if forces.shape != self.nb_subdomain_grid_pts: raise self.Error( ("force array has a different shape ({0}) than this halfspace'" "s nb_grid_pts ({1})").format( forces.shape, self.nb_subdomain_grid_pts)) # nopep8 self.real_buffer.array()[...] = -forces self.fftengine.fft(self.real_buffer, self.fourier_buffer) return self.greens_function * \ self.fourier_buffer.array() / self.area_per_pt def evaluate_k_force(self, disp): """ Computes the K-space forces (*not* pressures) due to a given displacement array. Keyword Arguments: disp -- a numpy array containing point displacements """ if disp.shape != self.nb_subdomain_grid_pts: raise self.Error( ("displacements array has a different shape ({0}) than this " "halfspace's nb_grid_pts ({1})").format( disp.shape, self.nb_subdomain_grid_pts)) # nopep8 self.real_buffer.array()[...] = disp self.fftengine.fft(self.real_buffer, self.fourier_buffer) return -self.surface_stiffness * \ self.fourier_buffer.array() * self.area_per_pt def evaluate_k_force_k(self, disp_k): """ Computes the K-space forces (*not* pressures) due to a given displacement array. Parameters: ----------- disp_k: complex nd_array a numpy array containing the rfft of point displacements """ return -self.surface_stiffness * disp_k * self.area_per_pt def evaluate_elastic_energy(self, forces, disp): """ computes and returns the elastic energy due to forces and displacements Arguments: forces -- array of forces disp -- array of displacements """ # pylint: disable=no-self-use return .5 * self.pnp.dot(np.ravel(disp), np.ravel(-forces)) def evaluate_scalar_product_k_space(self, ka, kb): r""" Computes the scalar product, i.e. the power, between the `a` and `b`, given their fourier representation. `Power theorem <https://ccrma.stanford.edu/~jos/mdft/Power_Theorem.html>`_: .. math :: P = \sum_{ij} a_{ij} b_{ij} = \frac{1}{n_x n_y}\sum_{ij} \tilde a_{ij} \overline{\tilde b_{ij}} Note that for `a`, `b` real, .. math :: P = \sum_{kl} Re(\tilde a_{kl}) Re(\tilde b_{kl}) + Im(\tilde a_{kl}) Im(\tilde b_{kl}) Parameters ---------- ka, kb: arrays of complex type and of size substrate.nb_fourier_grid_pts Fourier representation (output of a 2D rfftn) `a` (resp. `b`) (`nx, ny` real array) Returns ------- P The scalar product of a and b """ # ka and kb are the output of the 2D rfftn, that means the a # part of the transform is omitted because of the symetry along the # last dimension # # That's why the components whose symetrics have been omitted are # weighted with a factor of 2. # # The first column (indexes [...,0], wavevector 0 along the last # dimension) has no symetric # # When the number of points in the last dimension is even, the last # column (Nyquist Frequency) has also no symetric. # # The serial code implementation would look like this # if (self.nb_domain_grid_pts[-1] % 2 == 0) # return .5*(np.vdot(ka, kb).real + # # adding the data that has been omitted by rfftn # np.vdot(ka[..., 1:-1], kb[..., 1:-1]).real # # because of symetry # )/self.nb_pts # else : # return .5 * (np.vdot(ka, kb).real + # # adding the data that has been omitted by rfftn # # np.vdot(ka[..., 1:], kb[..., 1:]).real # # # because of symetry # # )/self.nb_pts # # Parallelized Version # The inner part of the fourier data should always be symetrized (i.e. # multiplied by 2). When the fourier subdomain contains boundary values # (wavevector 0 (even and odd) and ny//2 (only for odd)) these values # should only be added once if ka.size > 0: if self.fourier_locations[0] == 0: # First row of this fourier data is first of global data fact0 = 1 elif self.nb_fourier_grid_pts[0] > 1: # local first row is not the first in the global data fact0 = 2 else: fact0 = 0 if self.fourier_locations[0] == 0 and \ self.nb_fourier_grid_pts[0] == 1: factend = 0 elif (self.nb_domain_grid_pts[0] % 2 == 1): # odd number of points, last row have always to be symmetrized factend = 2 elif self.fourier_locations[0] + \ self.nb_fourier_grid_pts[0] - 1 == \ self.nb_domain_grid_pts[0] // 2: # last row of the global rfftn already contains it's symmetric factend = 1 # print("last Element of the even data has to be accounted # only once") else: factend = 2 # print("last element of this local slice is not last element # of the total global data") # print("fact0={}".format(fact0)) # print("factend={}".format(factend)) if self.nb_fourier_grid_pts[0] > 2: factmiddle = 2 else: factmiddle = 0 # vdot(a, b) = conj(a) . b locsum = (factmiddle * np.vdot(ka[1:-1, ...], kb[1:-1, ...]).real + fact0 * np.vdot(ka[0, ...], kb[0, ...]).real + factend * np.vdot(ka[-1, ...], kb[-1, ...]).real) / np.prod( self.nb_domain_grid_pts) # nopep8 # We divide by the total number of points to get the appropriate # normalisation of the Fourier transform (in numpy the division by # happens only at the inverse transform) else: # This handles the case where the processor holds an empty # subdomain locsum = np.array([], dtype=ka.real.dtype) # print(locsum) return self.pnp.sum(locsum) def evaluate_elastic_energy_k_space(self, kforces, kdisp): r""" Computes the Energy due to forces and displacements using their Fourier representation. .. math :: E_{el} &= - \frac{1}{2} \sum_{ij} u_{ij} f_{ij} &= - \frac{1}{2} \frac{1}{n_x n_y} \sum_{kl} \tilde u{kl} \overline{\tilde f_{kl}} (:math:`\tilde f_{ij} = - \tilde K_{ijkl} u`) In a parallelized code kforces and kdisp contain only the slice attributed to this processor Parameters ---------- kforces: array of complex type and of size substrate.nb_fourier_grid_pts Fourier representation (output of a 2D rfftn) of the forces acting on the grid points kdisp: array of complex type and of physical_sizes substrate.nb_fourier_grid_pts Fourier representation (output of a 2D rfftn) of the displacements of the grid points Returns ------- E The elastic energy due to the forces and displacements """ # noqa: E501, W291, W293 return -0.5 * self.evaluate_scalar_product_k_space(kdisp, kforces) def evaluate(self, disp, pot=True, forces=False): """Evaluates the elastic energy and the point forces Keyword Arguments: disp -- array of distances pot -- (default True) if true, returns potential energy forces -- (default False) if true, returns forces """ force = potential = None if forces: force = self.evaluate_force(disp) if pot: potential = self.evaluate_elastic_energy(force, disp) elif pot: kforce = self.evaluate_k_force(disp) # TODO: OPTIMISATION: here kdisp is computed twice, because it's # needed in kforce self.real_buffer.array()[...] = disp self.fftengine.fft(self.real_buffer, self.fourier_buffer) potential = self.evaluate_elastic_energy_k_space( kforce, self.fourier_buffer.array()) return potential, force def evaluate_k(self, disp_k, pot=True, forces=False): """Evaluates the elastic energy and the point forces Keyword Arguments: disp -- array of distances pot -- (default True) if true, returns potential energy forces -- (default False) if true, returns forces """ potential = None if forces: force_k = self.evaluate_k_force_k(disp_k) if pot: potential = self.evaluate_elastic_energy_k_space( force_k, disp_k) elif pot: force_k = self.evaluate_k_force_k(disp_k) potential = self.evaluate_elastic_energy_k_space(force_k, disp_k) return potential, force_k
def plastic_area(self): pnp = Reduction(self._communicator) return pnp.sum(np.count_nonzero(self.__h_pl)) * self.area_per_pt