File size: 8,565 Bytes
d68c650
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
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