1+ # This script uses Paraview's pvpython to take output from a global mantle
2+ # convection model and extract velocities along user-specified boundaries.
3+ # Next, the script uses the extracted velocities to create ASCII files which
4+ # can be applied as boundary conditions in regional models to account for
5+ # far-field effects outside of the regional model boundaries. This is achieved
6+ # by specifying the bounds of the regional model, slicing through the
7+ # global model along planes which coincide with the location of the
8+ # regional model boundaries, and saving the velocities at each point
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.
11+
12+ # The user must specify the following parameters in the script:
13+ # 1. The location of the .pvd file for a global convection model
14+ # 2. The output directory where the ASCII files will be saved
15+ # 3. The refinement level of the global model
16+ # 4. The radial resolution of the regional model
17+ # 5. The lateral resolution of the regional model
18+ # 6. The maximum and minimum radius, latitude, and longitude for the regional model.
19+
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
23+ # latitude, 190 degrees longitude to 230 degrees longitude, and from a
24+ # radius of 5070 km to a radius of 6370 km. Before running this script,
25+ # make sure that you run the S2ORTS cookbook and generate the solution.pvd file.
26+
27+ # This script defines 3 functions
28+ # 1. spherical_to_cartesian, which converts from spherical coordinates to Cartesian
29+ # coordinates
30+ # 2. cartesian_to_spherical, which converts from Cartesian coordinates to spherical
31+ # coordinates
32+ # 3. slice_plane_calculator, which determines the normal to the plane that defines
33+ # the east and west model boundaries in the regional chunk.
34+
35+ # Import packages
36+ import numpy as np
37+ import pandas as pd
38+ import os
39+ from paraview import simple
40+ from paraview import servermanager
41+
42+ def spherical_to_cartesian (radii , latitudes , longitudes , for_slice ):
43+ """
44+ Converts from spherical coordinates to Cartesian coordinates.
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
48+ for_slice: boolean, if false the provided points are directly
49+ converted into Cartesian coordinates. If true, radii, latitudes,
50+ and longitudes must be arrays, and the function returns a uniform
51+ structured grid defining the slice.
52+ Returns x, y, z, in m
53+ """
54+
55+ if for_slice :
56+ cartesian_coordinates = []
57+ for lat in latitudes :
58+ for lon in longitudes :
59+ for r in radii :
60+ x = r * np .sin (np .deg2rad (90 - lat )) * np .cos (np .deg2rad (lon ))
61+ y = r * np .sin (np .deg2rad (90 - lat )) * np .sin (np .deg2rad (lon ))
62+ z = r * np .cos (np .deg2rad (90 - lat ))
63+
64+ if lon == 0 :
65+ y = 0
66+ elif lon == np .pi / 2 :
67+ x = 0
68+ if lat == np .pi / 2 :
69+ z = 0
70+ cartesian_coordinates .append ([x , y , z ])
71+
72+ return np .array (cartesian_coordinates )
73+
74+ else :
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 ))
78+
79+ return np .array ([x , y , z ])
80+
81+ def cartesian_to_spherical (x , y , z ):
82+ """
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
85+ """
86+ r = np .sqrt (x ** 2 + y ** 2 + z ** 2 )
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
91+
92+ return r , latitude , longitude
93+
94+ def slice_plane_calculator (boundary_name , radius_bounds , latitude_bounds , longitude_bounds ):
95+ """
96+ Calculates the normal of a plane which is used for slicing the global models. This is
97+ achieved by defining three points on either the east or west model boundary using the
98+ values provided by radius_bounds, latitude_bounds, and longitude_bounds.
99+ boundary_name: the name of the model boundary
100+ radius_bounds: the maximum and minimum radius of the regional models
101+ latitude_bounds: the maximum and minimum latitude of the regional models
102+ longitude_bounds: the maximum and minimum longitude of the regional models
103+ """
104+ # Define the 3 points on the west or east boundary. If west, we are on the
105+ # minimum longitude, and if east we are on the maximum longitude.
106+ if boundary_name == "west" :
107+ spherical_point_1 = np .array ([np .max (radius_bounds ), \
108+ np .max (latitude_bounds ), \
109+ np .min (longitude_bounds )])
110+ spherical_point_2 = np .array ([np .max (radius_bounds ), \
111+ np .min (latitude_bounds ), \
112+ np .min (longitude_bounds )])
113+ spherical_point_3 = np .array ([np .min (radius_bounds ), \
114+ np .max (latitude_bounds ), \
115+ np .min (longitude_bounds )])
116+
117+ elif boundary_name == "east" :
118+ spherical_point_1 = np .array ([np .max (radius_bounds ), \
119+ np .max (latitude_bounds ), \
120+ np .max (longitude_bounds )])
121+ spherical_point_2 = np .array ([np .max (radius_bounds ), \
122+ np .min (latitude_bounds ), \
123+ np .max (longitude_bounds )])
124+ spherical_point_3 = np .array ([np .min (radius_bounds ), \
125+ np .max (latitude_bounds ), \
126+ np .max (longitude_bounds )])
127+
128+ else :
129+ raise Exception ("Unknown boundary name: " + boundary_name )
130+ # Convert spherical points to Cartesian
131+ cartesian_point_1 = spherical_to_cartesian (spherical_point_1 [0 ], \
132+ spherical_point_1 [1 ], \
133+ spherical_point_1 [2 ], \
134+ for_slice = False )
135+ cartesian_point_2 = spherical_to_cartesian (spherical_point_2 [0 ], \
136+ spherical_point_2 [1 ], \
137+ spherical_point_2 [2 ], \
138+ for_slice = False )
139+ cartesian_point_3 = spherical_to_cartesian (spherical_point_3 [0 ], \
140+ spherical_point_3 [1 ], \
141+ spherical_point_3 [2 ], \
142+ for_slice = False )
143+ # Calculate 2 in-plane orthogonal vectors using the 3 Cartesian points
144+ vector_1_2 = cartesian_point_2 - cartesian_point_1
145+ vector_1_3 = cartesian_point_3 - cartesian_point_1
146+ # Taking the cross product yields a vector normal to the model boundary
147+ normal_vector_to_plane = np .cross (vector_1_2 , vector_1_3 )
148+ # Normalize
149+ unit_normal = normal_vector_to_plane / np .linalg .norm (normal_vector_to_plane )
150+
151+ return unit_normal
152+
153+ ####################################################################################################################################
154+
155+ """ Usage: This script requires 8 input arguments:
156+ input_data: solution file for the global model (*.pvd)
157+ output_directory: Where the .txt files for each boundary are saved
158+ refinement_level: The number of mesh refinements in the global model
159+ output_radius_resolution: The radial resolution of the regional slice (in meters)
160+ output_lateral_resolution: The lateral resolution of the regional slice (in degrees)
161+ radius_bounds: Array with the minimum and maximum radius (meters) of the regional model
162+ latitude_bounds: Array with the minimum and maximum latitude (degrees) of the regional model
163+ longitude_bounds: Array with the minimum and maximum longitude (degrees) of the regional model
164+ """
165+
166+ # Define the input arguments for the S2ORTS cookbook
167+ input_data = "../../../cookbooks/initial-condition-S20RTS/output-S20RTS/solution.pvd"
168+ output_directory = "./regional_velocity_files/"
169+ refinement_level = 2
170+ output_radius_resolution = 10e3 # 10 km radial resolution
171+ output_lateral_resolution = 0.25 # 0.25 degree lateral resolution
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
183+
184+ # Loop over the 4 lateral boundaries: west, east, north, south.
185+ for boundary_name in np .array (["west" , "east" , "north" , "south" ]):
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.
190+ output_datafile = output_directory + "chunk_3d_" + boundary_name + ".0.txt"
191+
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.
198+ if boundary_name == "west" or boundary_name == "east" :
199+ cut_slice = simple .Slice (Input = model )
200+ cut_slice .SliceType .Normal = slice_plane_calculator (boundary_name , \
201+ radius_bounds , \
202+ latitude_bounds , \
203+ longitude_bounds )
204+ cut_slice .UpdatePipeline ()
205+
206+ # For the north and south boundary, use the threshold filter at a constant latitude, since these
207+ # boundaries will not always lie on great circles.
208+ elif boundary_name == "north" or boundary_name == "south" :
209+ # Create a calculator filter to determine the latitude in the global models
210+ pythonCalculator = simple .PythonCalculator (registrationName = "pythonCalculator" , Input = model )
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"
218+ pythonCalculator .ArrayName = "latitude"
219+ pythonCalculator .UpdatePipeline () # Apply the filter
220+
221+ # Create a threshold filter
222+ cut_slice = simple .Threshold (Input = pythonCalculator )
223+ cut_slice .Scalars = ("POINTS" , "latitude" ) # Threshold the latitude variable
224+ cut_slice .ThresholdMethod = "Between" # Specify the 'between' method for the threshold filter
225+
226+ # Threshold on either side of the maximum lat_bound (north) or the minimum lat_bound (south)
227+ # based on the refinement_level of the global models.
228+ if boundary_name == "north" :
229+ cut_slice .LowerThreshold = np .max (latitude_bounds ) - (180 / 2 ** (refinement_level + 1 ))
230+ cut_slice .UpperThreshold = np .max (latitude_bounds ) + (180 / 2 ** (refinement_level + 1 ))
231+
232+ if boundary_name == "south" :
233+ cut_slice .LowerThreshold = np .min (latitude_bounds ) - (180 / 2 ** (refinement_level + 1 ))
234+ cut_slice .UpperThreshold = np .min (latitude_bounds ) + (180 / 2 ** (refinement_level + 1 ))
235+
236+ cut_slice .UpdatePipeline ()
237+
238+ else :
239+ raise Exception ("Unknown boundary name: " + boundary_name )
240+
241+ # Create an array for the radius of the regional slice
242+ radius_array = np .arange (min (radius_bounds ), max (radius_bounds ) + output_radius_resolution , output_radius_resolution )
243+
244+ # Create arrays for the latitude and longitude for the regional slices depending on which boundary is
245+ # currently within the loop
246+ if boundary_name == "west" :
247+ latitude_array = np .arange (min (latitude_bounds ), max (latitude_bounds ) + output_lateral_resolution , output_lateral_resolution )
248+ longitude_array = np .array ([np .min (longitude_bounds )])
249+
250+ elif boundary_name == "east" :
251+ latitude_array = np .arange (min (latitude_bounds ), max (latitude_bounds ) + output_lateral_resolution , output_lateral_resolution )
252+ longitude_array = np .array ([np .max (longitude_bounds )])
253+
254+ elif boundary_name == "north" :
255+ latitude_array = np .array ([np .max (latitude_bounds )])
256+ longitude_array = np .arange (min (longitude_bounds ), max (longitude_bounds ) + output_lateral_resolution , output_lateral_resolution )
257+
258+ elif boundary_name == "south" :
259+ latitude_array = np .array ([np .min (latitude_bounds )])
260+ longitude_array = np .arange (min (longitude_bounds ), max (longitude_bounds ) + output_lateral_resolution , output_lateral_resolution )
261+
262+ # Calculate the cartesian coordinates on the regional slice
263+ cartesian_slice_coords = spherical_to_cartesian (radius_array , np .flip (latitude_array ), longitude_array , for_slice = True )
264+
265+ # Save Cartesian coordinates to .csv file for use later
266+ np .savetxt (temp_datafile , X = cartesian_slice_coords , delimiter = ',' , header = "x,y,z" , comments = '' )
267+
268+ # This opens the .csv as a paraview object
269+ reader = simple .OpenDataFile (temp_datafile )
270+ reader .UpdatePipeline ()
271+
272+ # Create a TableToPoints filter that will be used later for storing interpolated data from the slices
273+ tabletopoints = servermanager .filters .TableToPoints ()
274+
275+ # Assign the TableToPoints object the points of the .csv file
276+ tabletopoints .XColumn = 'x'
277+ tabletopoints .YColumn = 'y'
278+ tabletopoints .ZColumn = 'z'
279+ tabletopoints .Input = reader
280+ tabletopoints .UpdatePipeline ()
281+
282+ # Take the cut_slice object and interpolate the fields onto the TableToPoints filter above.
283+ 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`
286+ resample .ComputeTolerance = True
287+ resample .UpdatePipeline ()
288+
289+ # Take the points defined for the interpolated slice, and resave it in the .csv file.
290+ writer = simple .DataSetCSVWriter ()
291+ writer .Input = resample
292+ writer .WriteTimeSteps = 1
293+ writer .FileName = temp_datafile
294+ writer .UpdatePipeline ()
295+
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.
298+ dataframe = pd .read_csv (temp_datafile )
299+ x_slice = dataframe ["Points:0" ].to_numpy ()
300+ y_slice = dataframe ["Points:1" ].to_numpy ()
301+ z_slice = dataframe ["Points:2" ].to_numpy ()
302+
303+ vx_slice = dataframe ["velocity:0" ].to_numpy ()
304+ vy_slice = dataframe ["velocity:1" ].to_numpy ()
305+ vz_slice = dataframe ["velocity:2" ].to_numpy ()
306+
307+ # Convert the cartesian points to spherical points
308+ r_slice , theta_slice , phi_slice = cartesian_to_spherical (x_slice , y_slice , z_slice )
309+
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.
313+ if boundary_name == "west" or boundary_name == "east" :
314+ # ASPECT expects colatitude, so convert theta_slice to colatitude
315+ into_ASCII = np .array ([np .round (r_slice , - 3 ), \
316+ np .deg2rad (np .round (90 - theta_slice , 2 )), \
317+ vx_slice , \
318+ vy_slice , \
319+ vz_slice ]).T
320+ header = "POINTS: " + str (len (radius_array )) + " " + str (len (latitude_array ))
321+
322+ elif boundary_name == "north" or boundary_name == "south" :
323+ into_ASCII = np .array ([np .round (r_slice , - 3 ), \
324+ np .deg2rad (np .round (phi_slice , 2 )), \
325+ vx_slice , \
326+ vy_slice , \
327+ vz_slice ]).T
328+ header = "POINTS: " + str (len (radius_array )) + " " + str (len (longitude_array ))
329+
330+ # Save into an ASCII file and remove the temporary .csv files
331+ os .remove (temp_datafile )
332+ np .savetxt (fname = output_datafile , X = into_ASCII , fmt = '%.5e' , header = header )
333+
0 commit comments