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.
3839from paraview import simple
3940from 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
8081def 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
9294def 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
165169refinement_level = 2
166170output_radius_resolution = 10e3 # 10 km radial resolution
167171output_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.
173185for 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
0 commit comments