Skip to content

Commit 325b86c

Browse files
add changelog file
1 parent 5288eed commit 325b86c

File tree

2 files changed

+81
-48
lines changed

2 files changed

+81
-48
lines changed

contrib/python/scripts/extract_local_velocity.py

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
# by specifying the bounds of the regional model, slicing through the
77
# global model along planes which coincide with the location of the
88
# regional model boundaries, and saving the velocities at each point
9-
# into an ASCII file.
9+
# into an ASCII file. The regional model is expected to be a 3D spherical
10+
# chunk, and the global model is expected to be a 3D spherical shell.
1011

1112
# The user must specify the following parameters in the script:
12-
# 1. The location of the .pvd file for a global convection model
13+
# 1. The location of the .pvd file for a global convection model
1314
# 2. The output directory where the ASCII files will be saved
1415
# 3. The refinement level of the global model
1516
# 4. The radial resolution of the regional model
1617
# 5. The lateral resolution of the regional model
1718
# 6. The maximum and minimum radius, latitude, and longitude for the regional model.
1819

19-
# This shows an example for applying this script to the global model
20-
# presented in the S2ORTS cookbook, and applying the extracted velocities
21-
# to a regional model that spans from 20 degrees latitude to 50 degrees
20+
# The default settings inside this script are for an example applying this script to
21+
# the global model presented in the S2ORTS cookbook, and applying the extracted
22+
# velocities to a regional model that spans from 20 degrees latitude to 50 degrees
2223
# latitude, 190 degrees longitude to 230 degrees longitude, and from a
2324
# radius of 5070 km to a radius of 6370 km. Before running this script,
2425
# make sure that you run the S2ORTS cookbook and generate the solution.pvd file.
@@ -38,24 +39,24 @@
3839
from paraview import simple
3940
from paraview import servermanager
4041

41-
def spherical_to_cartesian(radiuss, latitudes, longitudes, for_slice):
42+
def spherical_to_cartesian(radii, latitudes, longitudes, for_slice):
4243
"""
4344
Converts from spherical coordinates to Cartesian coordinates.
44-
radiuss: the radius, in m
45-
latitudes: the latitude, in degrees
46-
longitudes: the longitude, in degrees
45+
radii: the radius, in m
46+
latitudes: the latitude, in degrees ranging from -90 to 90
47+
longitudes: the longitude, in degrees ranging from 0 to 360
4748
for_slice: boolean, if false the provided points are directly
48-
converted into Cartesian coordinates. If true, radiuss, latitudes,
49+
converted into Cartesian coordinates. If true, radii, latitudes,
4950
and longitudes must be arrays, and the function returns a uniform
5051
structured grid defining the slice.
51-
Returns x, y, z
52+
Returns x, y, z, in m
5253
"""
5354

5455
if for_slice:
5556
cartesian_coordinates = []
5657
for lat in latitudes:
5758
for lon in longitudes:
58-
for r in radiuss:
59+
for r in radii:
5960
x = r * np.sin(np.deg2rad(90 - lat)) * np.cos(np.deg2rad(lon))
6061
y = r * np.sin(np.deg2rad(90 - lat)) * np.sin(np.deg2rad(lon))
6162
z = r * np.cos(np.deg2rad(90 - lat))
@@ -71,23 +72,24 @@ def spherical_to_cartesian(radiuss, latitudes, longitudes, for_slice):
7172
return np.array(cartesian_coordinates)
7273

7374
else:
74-
x = radiuss * np.sin(np.deg2rad(90 - latitudes)) * np.cos(np.deg2rad(longitudes))
75-
y = radiuss * np.sin(np.deg2rad(90 - latitudes)) * np.sin(np.deg2rad(longitudes))
76-
z = radiuss * np.cos(np.deg2rad(90 - latitudes))
75+
x = radii * np.sin(np.deg2rad(90 - latitudes)) * np.cos(np.deg2rad(longitudes))
76+
y = radii * np.sin(np.deg2rad(90 - latitudes)) * np.sin(np.deg2rad(longitudes))
77+
z = radii * np.cos(np.deg2rad(90 - latitudes))
7778

7879
return np.array([x, y, z])
7980

8081
def cartesian_to_spherical(x, y, z):
8182
"""
82-
Takes an x, y, z and converts it to spherical coordinates. Returns r, theta, phi
83+
Takes an x, y, z point and converts it to spherical coordinates.
84+
Returns r in m, latitude in degrees, and longitude, ranging from 0 to 360, in degrees
8385
"""
8486
r = np.sqrt(x**2 + y**2 + z**2)
85-
theta = 90 - np.rad2deg( np.arccos( z / (np.sqrt(x**2 + y**2 + z**2)) ) )
86-
phi = np.sign(y) * np.rad2deg(np.arccos( x / np.sqrt(x**2 + y**2) ))
87-
phi[np.where(phi < 0)] = phi[np.where(phi < 0)] + 360
88-
phi[np.where(phi == 0)] = 180
87+
latitude = 90 - np.rad2deg( np.arccos( z / (np.sqrt(x**2 + y**2 + z**2)) ) )
88+
longitude = np.sign(y) * np.rad2deg(np.arccos( x / np.sqrt(x**2 + y**2) ))
89+
longitude[np.where(longitude < 0)] = longitude[np.where(longitude < 0)] + 360
90+
longitude[np.where(longitude == 0)] = 180
8991

90-
return r, theta, phi
92+
return r, latitude, longitude
9193

9294
def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longitude_bounds):
9395
"""
@@ -112,7 +114,7 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
112114
np.max(latitude_bounds), \
113115
np.min(longitude_bounds)])
114116

115-
if boundary_name == "east":
117+
elif boundary_name == "east":
116118
spherical_point_1 = np.array([np.max(radius_bounds), \
117119
np.max(latitude_bounds), \
118120
np.max(longitude_bounds)])
@@ -123,6 +125,8 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
123125
np.max(latitude_bounds), \
124126
np.max(longitude_bounds)])
125127

128+
else:
129+
raise Exception("Unknown boundary name: " + boundary_name)
126130
# Convert spherical points to Cartesian
127131
cartesian_point_1 = spherical_to_cartesian(spherical_point_1[0], \
128132
spherical_point_1[1], \
@@ -165,19 +169,32 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
165169
refinement_level = 2
166170
output_radius_resolution = 10e3 # 10 km radial resolution
167171
output_lateral_resolution = 0.25 # 0.25 degree lateral resolution
168-
radius_bounds = np.array([5070e3, 6370e3])
169-
latitude_bounds = np.array([20, 50])
170-
longitude_bounds = np.array([190, 230])
172+
radius_bounds = np.array([4770e3, 6360e3]) # Radius bounds of the regional model
173+
latitude_bounds = np.array([-20, -55]) # Latitude bounds of the regional model
174+
longitude_bounds = np.array([152, 210]) # Longitude bounds of the regional model
175+
176+
# Load in the global model
177+
model = simple.OpenDataFile(input_data)
178+
179+
# Only load in the mesh-points and the velocity fields for computational efficiency, to load
180+
# more fields, add entries to this array or just comment the line to load all solution fields.
181+
model.PointArrays = ['Points', 'velocity']
182+
model.UpdatePipeline() # Apply the filter
171183

172-
# Loop over the 4 lateral boundaries: west, east, north, south
184+
# Loop over the 4 lateral boundaries: west, east, north, south.
173185
for boundary_name in np.array(["west", "east", "north", "south"]):
174-
temp_datafile = output_directory + boundary_name + "_temp.csv"
186+
# To prescribe the velocity on the boundaries of a 3D spherical chunk in ASPECT, the ASCII files must be
187+
# named following this convention: chunk_3d_%s.%d.txt, where %s is the name of the model boundary, and
188+
# %d is the timestep that the ASCII file will be applied. This python script is intended to be used for
189+
# instantaneous models, and so %d is hardcoded to 0 when setting the variable output file name.
175190
output_datafile = output_directory + "chunk_3d_" + boundary_name + ".0.txt"
176-
model = simple.OpenDataFile(input_data)
177-
model.PointArrays = ['Points', 'velocity']
178-
model.UpdatePipeline()
179191

180-
# For the east and west boundary, use the slice filter since these boundaries lie on great circles
192+
# pvpython will save the data in a .csv file, which will not be formatted correctly for use in ASPECT.
193+
# This script creates a temporary file to store the pvpython output, then deletes the file later.
194+
temp_datafile = output_directory + boundary_name + "_boundary_paraview_slice_temp.csv"
195+
196+
# For the east and west boundary, use the slice filter to extract velocities since these boundaries
197+
# lie on great circles.
181198
if boundary_name == "west" or boundary_name == "east":
182199
cut_slice = simple.Slice(Input=model)
183200
cut_slice.SliceType.Normal = slice_plane_calculator(boundary_name, \
@@ -188,19 +205,23 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
188205

189206
# For the north and south boundary, use the threshold filter at a constant latitude, since these
190207
# boundaries will not always lie on great circles.
191-
if boundary_name == "north" or boundary_name == "south":
208+
elif boundary_name == "north" or boundary_name == "south":
192209
# Create a calculator filter to determine the latitude in the global models
193210
pythonCalculator = simple.PythonCalculator(registrationName="pythonCalculator", Input=model)
194-
pythonCalculator.ArrayAssociation = "Point Data" # "Point Data" not "Cell Data"
195-
pythonCalculator.CopyArrays = True # Copy all of the other variables into the calculator filter
196-
pythonCalculator.Expression = "90 - np.arccos( points[:, 2] / (sqrt(points[:, 0]**2 + points[:, 1]**2 + points[:, 2]**2)) ) * 180 / np.pi" # Calculate the latitude
211+
pythonCalculator.ArrayAssociation = "Point Data" # "Point Data" since the velocity is output on points
212+
213+
# Copy all of the other variables into the calculator filter. If this is set to False, then the
214+
# velocity field will not be passed through once the pythonCalculator is applied
215+
pythonCalculator.CopyArrays = True
216+
# Calculate the latitude
217+
pythonCalculator.Expression = "90 - np.arccos( points[:, 2] / (sqrt(points[:, 0]**2 + points[:, 1]**2 + points[:, 2]**2)) ) * 180 / np.pi"
197218
pythonCalculator.ArrayName = "latitude"
198-
pythonCalculator.UpdatePipeline()
219+
pythonCalculator.UpdatePipeline() # Apply the filter
199220

200-
# Create threshold filter
221+
# Create a threshold filter
201222
cut_slice = simple.Threshold(Input=pythonCalculator)
202223
cut_slice.Scalars = ("POINTS", "latitude") # Threshold the latitude variable
203-
cut_slice.ThresholdMethod = "Between"
224+
cut_slice.ThresholdMethod = "Between" # Specify the 'between' method for the threshold filter
204225

205226
# Threshold on either side of the maximum lat_bound (north) or the minimum lat_bound (south)
206227
# based on the refinement_level of the global models.
@@ -213,7 +234,10 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
213234
cut_slice.UpperThreshold = np.min(latitude_bounds) + (180 / 2**(refinement_level + 1))
214235

215236
cut_slice.UpdatePipeline()
216-
237+
238+
else:
239+
raise Exception("Unknown boundary name: " + boundary_name)
240+
217241
# Create an array for the radius of the regional slice
218242
radius_array = np.arange(min(radius_bounds), max(radius_bounds) + output_radius_resolution, output_radius_resolution)
219243

@@ -223,15 +247,15 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
223247
latitude_array = np.arange(min(latitude_bounds), max(latitude_bounds) + output_lateral_resolution, output_lateral_resolution)
224248
longitude_array = np.array([np.min(longitude_bounds)])
225249

226-
if boundary_name == "east":
250+
elif boundary_name == "east":
227251
latitude_array = np.arange(min(latitude_bounds), max(latitude_bounds) + output_lateral_resolution, output_lateral_resolution)
228252
longitude_array = np.array([np.max(longitude_bounds)])
229253

230-
if boundary_name == "north":
254+
elif boundary_name == "north":
231255
latitude_array = np.array([np.max(latitude_bounds)])
232256
longitude_array = np.arange(min(longitude_bounds), max(longitude_bounds) + output_lateral_resolution, output_lateral_resolution)
233257

234-
if boundary_name == "south":
258+
elif boundary_name == "south":
235259
latitude_array = np.array([np.min(latitude_bounds)])
236260
longitude_array = np.arange(min(longitude_bounds), max(longitude_bounds) + output_lateral_resolution, output_lateral_resolution)
237261

@@ -245,7 +269,7 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
245269
reader = simple.OpenDataFile(temp_datafile)
246270
reader.UpdatePipeline()
247271

248-
# Create a TableToPoints filter
272+
# Create a TableToPoints filter that will be used later for storing interpolated data from the slices
249273
tabletopoints = servermanager.filters.TableToPoints()
250274

251275
# Assign the TableToPoints object the points of the .csv file
@@ -257,6 +281,8 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
257281

258282
# Take the cut_slice object and interpolate the fields onto the TableToPoints filter above.
259283
resample = simple.ResampleWithDataset(SourceDataArrays=cut_slice, DestinationMesh=tabletopoints)
284+
# Allow pvpython to compute the tolerance for resampling. If set to False, the user can provide
285+
# a tolerance with the line `resample.Tolerance`
260286
resample.ComputeTolerance = True
261287
resample.UpdatePipeline()
262288

@@ -267,8 +293,8 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
267293
writer.FileName = temp_datafile
268294
writer.UpdatePipeline()
269295

270-
# Load the .csv file and extract the x, y, z coordinates and the x, y, and z
271-
# components of the velocity.
296+
# Load the .csv file with the interpolated slice velocity data and extract both
297+
# the x, y, z coordinates and the x, y, and z components of the velocity.
272298
dataframe = pd.read_csv(temp_datafile)
273299
x_slice = dataframe["Points:0"].to_numpy()
274300
y_slice = dataframe["Points:1"].to_numpy()
@@ -281,8 +307,9 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
281307
# Convert the cartesian points to spherical points
282308
r_slice, theta_slice, phi_slice = cartesian_to_spherical(x_slice, y_slice, z_slice)
283309

284-
# Create the header and the arrays for saving into a ASCII file. Round to ensure that
285-
# each entry for the radius and latitude/longitude is consistent.
310+
# Create the header and the arrays for saving into an ASCII file. Round to ensure that
311+
# each entry for the radius and latitude/longitude is consistent. The file will be named
312+
# and formatted in such a way that it can be directly applied to an ASPECT model.
286313
if boundary_name == "west" or boundary_name == "east":
287314
# ASPECT expects colatitude, so convert theta_slice to colatitude
288315
into_ASCII = np.array([np.round(r_slice, -3), \
@@ -300,7 +327,7 @@ def slice_plane_calculator(boundary_name, radius_bounds, latitude_bounds, longit
300327
vz_slice]).T
301328
header = "POINTS: " + str(len(radius_array)) + " " + str(len(longitude_array))
302329

303-
# Save into ASCII file and remove the temporary .csv files
330+
# Save into an ASCII file and remove the temporary .csv files
304331
os.remove(temp_datafile)
305332
np.savetxt(fname=output_datafile, X=into_ASCII, fmt='%.5e', header=header)
306333

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Added: A python script which uses Paraview's pvpython to extract velocities
2+
from a global model along user-specified boundaries and saves them to ASCII
3+
files which can then be applied as velocity boundary conditions in regional
4+
chunk models.
5+
<br>
6+
(Daniel Douglas, 2024/09/05)

0 commit comments

Comments
 (0)