def test_SmoothData1D(): """ test smoothing """ x = np.random.uniform(0, 1, 128) xs = np.linspace(0, 1, 16)[1:-1] s = SmoothData1D(x, np.ones_like(x)) np.testing.assert_allclose(s(xs), 1) s = SmoothData1D(x, x) np.testing.assert_allclose(s(xs), xs, atol=0.1) s = SmoothData1D(x, np.sin(x)) np.testing.assert_allclose(s(xs), np.sin(xs), atol=0.1)
def test_SmoothData1D(): """test smoothing""" x = np.random.uniform(0, 1, 128) xs = np.linspace(0, 1, 16)[1:-1] s = SmoothData1D(x, np.ones_like(x)) np.testing.assert_allclose(s(xs), 1) s = SmoothData1D(x, x) np.testing.assert_allclose(s(xs), xs, atol=0.1) s = SmoothData1D(x, np.sin(x)) np.testing.assert_allclose(s(xs), np.sin(xs), atol=0.1) assert -0.1 not in s assert x.min() in s assert 0.5 in s assert x.max() in s assert 1.1 not in s x = np.arange(3) y = [0, 1, np.nan] s = SmoothData1D(x, y) assert s(0.5) == pytest.approx(0.5)
def get_length_scale( scalar_field: ScalarField, method: str = "structure_factor_maximum", full_output: bool = False, smoothing: Optional[float] = None, ) -> Union[float, Tuple[float, Any]]: """Calculates a length scale associated with a phase field Args: scalar_field (:class:`~pde.fields.ScalarField`): The scalar_field being analyzed method (str): A string determining which method is used to calculate the length scale.Valid options are `structure_factor_maximum` (numerically determine the maximum in the structure factor) and `structure_factor_mean` (calculate the mean of the structure factor). full_output (bool): Flag determining whether additional data is returned. The format of the returned data depends on the method. smoothing (float, optional): Length scale that determines the smoothing of the radially averaged structure factor. If `None` it is automatically determined from the typical discretization of the underlying grid. This parameter is only used if `method = 'structure_factor_maximum'` Returns: float: The determine length scale If `full_output = True`, a tuple with the length scale and an additional object with further information is returned. """ logger = logging.getLogger(__name__) if method == "structure_factor_mean" or method == "structure_factor_average": # calculate the structure factor k_mag, sf = get_structure_factor(scalar_field) length_scale = np.sum(sf) / np.sum(k_mag * sf) if full_output: return length_scale, sf elif method == "structure_factor_maximum" or method == "structure_factor_peak": # calculate the structure factor k_mag, sf = get_structure_factor(scalar_field, smoothing=None) # smooth the structure factor if smoothing is None: smoothing = 0.01 * scalar_field.grid.typical_discretization sf_smooth = SmoothData1D(k_mag, sf, sigma=smoothing) # find the maximum with warnings.catch_warnings(): warnings.simplefilter("ignore") max_est = k_mag[np.argmax(sf)] bracket = np.array([0.2, 1, 5]) * max_est logger.debug(f"Search maximum of structure factor in interval {bracket}") try: result = optimize.minimize_scalar( lambda x: -sf_smooth(x), bracket=bracket ) except Exception: logger.exception("Could not determine maximal structure factor") length_scale = np.nan else: if not result.success: logger.warning( "Maximization of structure factor resulted in the following " f"message: {result.message}" ) length_scale = 1 / result.x if full_output: return length_scale, sf_smooth else: raise ValueError( f"Method {method} is not defined. Valid values are `structure_factor_mean` " "and `structure_factor_maximum`" ) # return only the length scale with out any additional information return length_scale # type: ignore
def get_structure_factor( scalar_field: ScalarField, smoothing: Union[None, float, str] = "auto", wave_numbers: Union[Sequence[float], str] = "auto", add_zero: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Calculates the structure factor associated with a field Here, the structure factor is basically the power spectral density of the field `scalar_field` normalized so that re-gridding or rescaling the field does not change the result. Args: scalar_field (:class:`~pde.fields.ScalarField`): The scalar_field being analyzed smoothing (float, optional): Length scale that determines the smoothing of the radially averaged structure factor. If omitted, the full data about the discretized structure factor is returned. The special value `auto` calculates a value automatically. wave_numbers (list of floats, optional): The magnitude of the wave vectors at which the structure factor is evaluated. This only applies when smoothing is used. If `auto`, the wave numbers are determined automatically. add_zero (bool): Determines whether the value at k=0 (defined to be 1) should also be returned. Returns: (numpy.ndarray, numpy.ndarray): Two arrays giving the wave numbers and the associated structure factor """ logger = logging.getLogger(__name__) if not isinstance(scalar_field, ScalarField): raise TypeError( "Length scales can only be calculated for scalar " f"fields, not {scalar_field.__class__.__name__}" ) grid = scalar_field.grid if not isinstance(grid, CartesianGridBase): raise NotImplementedError( "Structure factor can currently only be calculated for Cartesian grids" ) if not all(grid.periodic): logger.warning( "Structure factor calculation assumes periodic boundary " "conditions, but not all grid dimensions are periodic" ) # do the n-dimensional Fourier transform and calculate the structure factor f1 = np_fftn(scalar_field.data, norm="ortho").flat[1:] flat_data = scalar_field.data.flat sf = np.abs(f1) ** 2 / np.dot(flat_data, flat_data) # an alternative calculation of the structure factor is # f2 = np_ifftn(scalar_field.data, norm='ortho').flat[1:] # sf = (f1 * f2).real # sf /= (scalar_field.data**2).sum() # but since this involves two FFT, it is probably slower # determine the (squared) components of the wave vectors k2s = [ np.fft.fftfreq(grid.shape[i], d=grid.discretization[i]) ** 2 for i in range(grid.dim) ] # calculate the magnitude k_mag = np.sqrt(reduce(np.add.outer, k2s)).flat[1:] no_wavenumbers = wave_numbers is None or ( isinstance(wave_numbers, str) and wave_numbers == "auto" ) if smoothing is not None and smoothing != "none": # construct the smoothed function of the structure factor if smoothing == "auto": smoothing = k_mag.max() / 128 smoothing = float(smoothing) # type: ignore sf_smooth = SmoothData1D(k_mag, sf, sigma=smoothing) if no_wavenumbers: # determine the wave numbers at which to evaluate it k_min = 2 / grid.cuboid.size.max() k_max = k_mag.max() k_mag = np.linspace(k_min, k_max, 128) else: k_mag = np.array(wave_numbers) # obtain the smoothed values at these points sf = sf_smooth(k_mag) elif not no_wavenumbers: logger.warning( "Argument `wave_numbers` is only used when `smoothing` is enabled." ) if add_zero: sf = np.r_[1, sf] k_mag = np.r_[0, k_mag] return k_mag, sf