import numpy as np def generate_piecewise_linear_function(num_pieces, lower, upper, delta = 0.3): """ Generates a piece-wise linear function on the interval (lower, upper) that is 2*pi-periodic. Args: num_pieces (int): Number of linear pieces in the function. lower (float): The lower range of the interval upper (float): The upper range of the interval delta (float): Parameter determining how rapidly the function varies between the grid points (i.e., modulates the slopes of the piecewise functions). Larger values mean more variability. Default is 0.3. Returns: function: A piece-wise linear function. """ # Generate equally spaced points in the interval (-pi, pi) x_points = np.linspace(lower, upper, num_pieces + 1) # Generate random y-values for each point y_points = np.zeros(num_pieces + 1) y_points[0] = np.random.uniform(-1, 1) for n in range(1, y_points.shape[0]): y_points[n] = y_points[n - 1] + 0.3 * np.random.uniform(-1, 1) y_points[0] = y_points[-1] min_y = y_points.min() # ensure y values are nonegative if min_y < 0: y_points -= min_y y_points += np.random.uniform(0, 0.5) # random (constant) offset def piecewise_linear(x): """ Evaluates the piece-wise linear function at a given x. Args: x (float): The x-coordinate at which to evaluate the function. Returns: float: The y-coordinate of the function at x. """ for i in range(num_pieces): if x_points[i] <= x < x_points[i + 1]: # Linear interpolation between the two points slope = (y_points[i + 1] - y_points[i]) / (x_points[i + 1] - x_points[i]) return slope * (x - x_points[i]) + y_points[i] return y_points[0] # For x = pi return piecewise_linear def generate_piecewise_constant_function(num_pieces, lower, upper): """ Generates a piece-wise constant function on the interval (lower, upper) that is 2*pi periodic. Args: num_pieces (int): Number of constant pieces in the function. lower (float): The lower range of the interval upper (float): The upper range of the interval Returns: function: A piece-wise constant function. """ # Generate equally spaced points in the interval (-pi, pi) x_points = np.linspace(lower, upper, num_pieces + 1) # Generate random y-values for each constant piece y_values = np.random.rand(num_pieces) * 2 - 0 # Random values between 0 and 1 # Ensure the function is 2*pi periodic y_values = np.append(y_values, y_values[0]) def piecewise_constant(x): """ Evaluates the piece-wise constant function at a given x. Args: x (float): The x-coordinate at which to evaluate the function. Returns: float: The y-coordinate of the function at x. """ for i in range(num_pieces): if x_points[i] <= x < x_points[i + 1]: return y_values[i] return y_values[0] # For x = pi return piecewise_constant def generate_piecewise_bc(x2_grid, x3_grid, num_pieces): """ Generates a piecewise linear function on the domain defined by x2_grid and x3_grid. Args: x2_grid (numpy.ndarray): A 2D array, x2 grid values x3_grid (numpy.ndarray): A 2D array, x3 grid values num_pieces (int): The number of pieces in the piecewise linear function. Returns: numpy.ndarray: A 2D array representing the piecewise linear function """ x2_fun = generate_piecewise_linear_function(num_pieces, lower = x2_grid.min(), upper = x2_grid.max()) x3_fun = generate_piecewise_linear_function(num_pieces, lower = x3_grid.min(), upper = x3_grid.max()) x2vals_1d = np.zeros_like(x2_grid[:, 0]) x3vals_1d = np.zeros_like(x3_grid[0, :]) for i in range(x2vals_1d.shape[0]): x2vals_1d[i] = x2_fun(x2_grid[i, 0]) for i in range(x3vals_1d.shape[0]): x3vals_1d[i] = x3_fun(x3_grid[0, i]) x2vals_2d, x3vals_2d = np.meshgrid(x2vals_1d, x3vals_1d, indexing = 'ij') return x2vals_2d * x3vals_2d def random_2d_gaussian(theta, phi): """ Generates a 2D Gaussian G(x,y), where x = np.cos(0.5 * freq_x * theta - phase_x) y = np.cos(0.5 * freq_y * phi - phase_y) Here, the frequencies and phases are randomly sampled, and (theta, phi) define a 2D meshgrid. Args: theta (numpy.ndarray): 2D array, meshgrid of the first coordinate phi (numpy.ndarray): 2D array, meshgrid of the second coordinate Returns: numpy.ndarray: A 2D array representing the values of the Gaussian on the grid. """ phase_x = np.random.uniform(0, 2 * np.pi) phase_y = np.random.uniform(0, 2 * np.pi) freq_x = np.random.randint(1, 2) freq_y = np.random.randint(1, 2) x = np.cos(0.5 * freq_x * theta - phase_x) y = np.cos(0.5 * freq_y * phi - phase_y) sigma_x = np.random.uniform(0.1, 3.0) sigma_y = np.random.uniform(0.1, 1.0) rho = 0 covariance_matrix = np.array([[sigma_x**2, rho * sigma_x * sigma_y], [rho * sigma_x * sigma_y, sigma_y**2]]) inv_sigma_xx = 1.0 / sigma_x**2 inv_sigma_yy = 1.0 / sigma_y**2 inv_sigma_xy = -rho / (sigma_x * sigma_y) if np.any(np.linalg.eigvals(covariance_matrix) < 0): raise ValueError('Covariance matrix is not positive semi-definite.') def gaussian_2d(x, y): return np.exp(-0.5 * (inv_sigma_xx * x**2 + inv_sigma_yy * y**2 + 2 * inv_sigma_xy * x * y)) gaussian_values = gaussian_2d(x, y) return gaussian_values def generate_random_functions(N, X, Y, num_terms = 16, min_freq = 1, max_freq = 16, func_gen_id = 0): """ Generates N random 2pi-periodic functions on a 2D grid as a Fourier series, with different types of modulation applied to the amplitudes. Args: N (int): Number of functions to generate. X (numpy.ndarray): 2D array representing the values of the first coordinate on the grid Y (numpy.ndarray): 2D array representing the values of the second coordinate on the grid num_terms (int, optional): Number of terms in the Fourier series expansion. Default is 16. min_freq (int, optional): Minimum frequency for the Fourier series terms. Default is 1. max_freq (int, optional): Maximum frequency for the Fourier series terms. Default is 16. func_gen_id (int, optional): Type of function to generate based on the decay of the expansion coefficients as frequency is increased. Values can range from -1 to 4. Default is 0. Returns: numpy.ndarray: A 3D numpy array of shape (N, nx, ny) containing the function values. Raises: ValueError: If max_freq is less than min_freq or if an invalid func_gen_id is provided. """ # Check if the maximum frequency is less than the minimum frequency if max_freq < min_freq: raise ValueError('max_freq cannot be less than min_freq') # Generate uniformly distributed functions if func_gen_id is -1 if func_gen_id == -1: F_batch = np.random.uniform(0, 1, size = (N,) + X.shape) return F_batch # Initialize the batch of functions with zeros F_batch = np.zeros((N,) + X.shape) # Loop through each function to be generated for n in range(N): # Add a cosine term with a half frequency with 20% chance if np.random.uniform(0, 1) < 0.2: amp_cos_half = np.random.uniform(0, 1) # Amplitude for cosine term phase_cos_half = np.random.uniform(0, 2 * np.pi) # Phase shift for cosine term F_batch[n] += amp_cos_half * np.cos(0.5 * X - phase_cos_half) # Fourier series for _ in range(num_terms): amplitude = np.random.uniform(-1, 1) # Random amplitude for y-component kx, ky = np.random.randint(min_freq, max_freq + 1, 2) # Frequencies for x and y components phase_x = np.random.uniform(0, 2 * np.pi) # Phase shift for x-component phase_y = np.random.uniform(0, 2 * np.pi) # Phase shift for y-component # Determine the coefficient amplitude based on the func_gen_id if func_gen_id == 0: # No decay applied to amplitude pass elif func_gen_id == 1: if np.random.uniform(0, 1) < 0.5: amplitude = amplitude / kx else: amplitude = amplitude / ky elif func_gen_id == 2: amplitude = amplitude / (kx * ky) elif func_gen_id == 3: amplitude = amplitude / (kx * kx * ky * ky) elif func_gen_id == 4: # Gaussian decay with random covariance matrix sxx = np.random.uniform(0.1, 1.0) syy = np.random.uniform(0.1, 1.0) sxy = np.random.uniform(0.1, 1.0) amplitude = amplitude * np.exp(-(sxx * kx**2 + syy * ky**2 + sxy * kx * ky)) else: raise ValueError( f'Invalid func_gen_id. Should be an integer in the range [-1, 4], but received {func_gen_id}') # Add the term to the nth function in the batch F_batch[n] += amplitude * np.cos(kx * X - phase_x) * np.cos(ky * Y - phase_y) # Adjust the function to ensure it's positive minF = np.min(F_batch[n]) if minF < 0: F_batch[n] -= minF return F_batch