From fe3cfb5986f2bf2f4c251fa843584d101d8dad74 Mon Sep 17 00:00:00 2001 From: Victor Oliveira Ferreira <65647639+voferreira@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:20:38 -0500 Subject: [PATCH] Update 3d small-scale rotating drum example and postprocessing (#1383) * Update small-scale-rotating-drum example doc * Fix save vtu functionality of lethe-pyvista-tools and update example accordingly * Final details on documentation and doc bug fix * Small fixes in the small 3d rotating drum documentation * Remove duplicated reference to fix warning --------- Co-authored-by: Oreste Marquis --- contrib/postprocessing/example.py | 2 +- .../lethe_pyvista_tools/__init__.py | 29 ++++++---- .../lethe_pyvista_tools/_create_copy.py | 28 ++++++++-- .../_get_cylindrical_coords.py | 4 +- .../lethe_pyvista_tools/_get_df.py | 5 +- .../_get_nearest_neighbors.py | 4 +- .../_mixing_index_doucet.py | 6 +- .../_mixing_index_nearest_neighbors.py | 8 +-- .../lethe_pyvista_tools/_modify_array.py | 35 +++++++++--- .../lethe_pyvista_tools/_sort_by_array.py | 4 +- .../lethe_pyvista_tools/_write_df_to_pvtu.py | 6 +- ...all-scale-rotating-drum-postprocessing.rst | 4 +- .../small-scale-rotating-drum.rst | 55 ++++++++++--------- .../3d-example_small_rotating_drum.py | 4 +- 14 files changed, 124 insertions(+), 70 deletions(-) diff --git a/contrib/postprocessing/example.py b/contrib/postprocessing/example.py index 862ee14ba0..da03bea5f8 100644 --- a/contrib/postprocessing/example.py +++ b/contrib/postprocessing/example.py @@ -37,7 +37,7 @@ # .list_pvtu and reads the '.pvtu' files inside the pointed folder as pyvista # dataframes. print('List of all .pvtu: ') -print(example.list_vtu) +print(example.list_pvtu) print('Time list, if transient: ') print(example.time_list) diff --git a/contrib/postprocessing/lethe_pyvista_tools/__init__.py b/contrib/postprocessing/lethe_pyvista_tools/__init__.py index 75e15d5906..a626af9cd5 100644 --- a/contrib/postprocessing/lethe_pyvista_tools/__init__.py +++ b/contrib/postprocessing/lethe_pyvista_tools/__init__.py @@ -5,6 +5,7 @@ # Import modules import pyvista as pv from tqdm import tqdm +import os # Define class: class lethe_pyvista_tools(): @@ -66,7 +67,9 @@ def __init__(self, case_path = '.', prm_file_name = '', pvd_name = '', prefix = self.time_list -> Returns the list of times corresponding to datasets. - self.list_vtu -> Returns the list of names of .pvtu files. + self.list_pvtu -> Returns the list of names of .pvtu files. + + self.list_vtu -> Returns the list of names of .vtu files for copy purposes. self.padding -> Returns the padding of pvtu file numbering. @@ -82,8 +85,7 @@ def __init__(self, case_path = '.', prm_file_name = '', pvd_name = '', prefix = self.padding = '0' if n_procs is None: - from os import cpu_count - self.n_procs = cpu_count() + self.n_procs = 1 else: self.n_procs = n_procs @@ -162,6 +164,9 @@ def __init__(self, case_path = '.', prm_file_name = '', pvd_name = '', prefix = else: self.prm_dict[clean_line[0]] = clean_line[1] + if 'output file name' not in self.prm_dict.keys(): + self.prm_dict['output file name'] = 'out' + print(f'Successfully constructed. To see the .prm dictionary, print($NAME.prm_dict)') # Define path where pvtu files are @@ -177,27 +182,31 @@ def __init__(self, case_path = '.', prm_file_name = '', pvd_name = '', prefix = self.time_list = self.reader.time_values # Create a list of all files' names - list_vtu = [pvd_datasets[x].path for x in range(len(pvd_datasets))] + list_pvtu = [pvd_datasets[x].path for x in range(len(pvd_datasets))] # Remove duplicates - list_vtu = list(dict.fromkeys(list_vtu)) + list_pvtu = list(dict.fromkeys(list_pvtu)) # Select data if last is None: - self.list_vtu = list_vtu[first::step] + self.list_pvtu = list_pvtu[first::step] self.time_list = self.time_list[first::step] self.first = first self.step = step self.last = len(self.time_list) - 1 self.pvd_datasets = self.reader.datasets[first::step] else: - self.list_vtu = list_vtu[first:last:step] + self.list_pvtu = list_pvtu[first:last:step] self.time_list = self.time_list[first:last:step] self.first = first self.step = step self.last = last self.pvd_datasets = self.reader.datasets[first:last:step] + #Define list of VTU files + list_vtu = self.list_pvtu.copy() + self.list_vtu = [x.replace('.pvtu', '.00000.vtu') for x in list_vtu] + if len(prefix) > 0: self.create_copy(prefix = prefix) @@ -216,9 +225,9 @@ def __init__(self, case_path = '.', prm_file_name = '', pvd_name = '', prefix = self.df = [] # Read PVTU data - n_pvtu = len(self.list_vtu) + n_pvtu = len(self.list_pvtu) pbar = tqdm(total = n_pvtu, desc="Reading PVTU files") - for i in range(len(self.list_vtu)): + for i in range(len(self.list_pvtu)): # Read dataframes from VTU files into df self.df.append(self.get_df) @@ -226,7 +235,7 @@ def __init__(self, case_path = '.', prm_file_name = '', pvd_name = '', prefix = self.df_available = True - print(f'Written .df[timestep] from timestep = 0 to timestep = {len(self.list_vtu)-1}') + print(f'Written .df[timestep] from timestep = 0 to timestep = {len(self.list_pvtu)-1}') # IMPORT FUNCTIONS: diff --git a/contrib/postprocessing/lethe_pyvista_tools/_create_copy.py b/contrib/postprocessing/lethe_pyvista_tools/_create_copy.py index ea724abcbf..536668d992 100644 --- a/contrib/postprocessing/lethe_pyvista_tools/_create_copy.py +++ b/contrib/postprocessing/lethe_pyvista_tools/_create_copy.py @@ -12,7 +12,7 @@ def create_copy(self, prefix): for line in pvd_in: # If line refers to a dataset - if "vtu" in line: + if "pvtu" in line: # For all read files for path in read_files_path_list: @@ -20,8 +20,8 @@ def create_copy(self, prefix): # If line matches one of the files if path in line: - # If vtu is in list_vtu - if line.split('file="')[1].split('"/>')[0] in self.list_vtu: + # If vtu is in list_pvtu + if line.split('file="')[1].split('"/>')[0] in self.list_pvtu: line = line.replace('file="', f'file="{prefix}') pvd_out.write(line) read_files_path_list.remove(path) @@ -32,18 +32,36 @@ def create_copy(self, prefix): pvd_out.write(line) # Make a copy of VTU files - n_vtu = len(self.list_vtu) + n_vtu = len(self.list_pvtu) pbar = tqdm(total = n_vtu, desc="Writing modified VTU and PVD files") + new_list_pvtu = [] new_list_vtu = [] - for i in range(len(self.list_vtu)): + for i in range(len(self.list_pvtu)): + new_vtu_reference = F' \n' + # Copy file + shutil.copy2(f'{self.path_output}/{self.list_pvtu[i]}', f'{self.path_output}/{prefix}{self.list_pvtu[i]}') + + # Change reference to VTU file in PVTU file + pvtu_content = [] + with open(f'{self.path_output}/{prefix}{self.list_pvtu[i]}') as pvtu_in: + pvtu_content = pvtu_in.readlines() + + with open(f'{self.path_output}/{prefix}{self.list_pvtu[i]}', 'w') as pvtu_out: + for line in pvtu_content: + if f'\n' + with open(f'{self.path_output}/{self.list_pvtu[i]}', 'r') as f: + # Read the file contents + lines = f.readlines() + + # Find the closing tag of the PPointData section + for j, line in enumerate(lines): + if line.strip() == "": + # Insert the new line just before the closing tag + lines.insert(j, new_pdata_line) + break - self.parallel_run(create_array, range(len(self.list_vtu)), tqdm_desc = f"Creating array {array_name}") + # Write the updated contents back to the file + with open(f'{self.path_output}/{self.list_pvtu[i]}', 'w') as file: + file.writelines(lines) + + df.save(f'{self.path_output}/{self.list_vtu[i]}', binary = False) + + self.parallel_run(create_array, range(len(self.list_pvtu)), tqdm_desc = f"Creating array {array_name}") else: if self.df_available: @@ -213,12 +232,12 @@ def modify_array_loop(i): new_array[k] = eval(array_values) # Assign new_array to pyvista dataframe - df[array_name] = new_array + df[array_name] = new_array.astype(np.float32) if self.df_available: self.df[i] = df else: - df.save(f'{self.path_output}/{self.list_vtu[i]}') + df.save(f'{self.path_output}/{self.list_vtu[i]}', binary = False) # If not time dependent, the condition and array_values will be applied # at the reference_time_step. @@ -289,7 +308,7 @@ def modify_array_loop(i): self.df[reference_time_step][array_name] = new_array else: df_reference[array_name] = new_array - df_reference.save(f'{self.path_output}/{self.list_vtu[reference_time_step]}') + df_reference.save(f'{self.path_output}/{self.list_vtu[reference_time_step]}', binary = False) # Create dictionary (map) based on reference_array reference_time_step_dict = dict(zip(df_reference[reference_array_name], df_reference[array_name])) @@ -318,4 +337,4 @@ def modify_array_loop(i): df[array_name][indices] = itemgetter(*keys)(reference_time_step_dict) df.save(f'{self.path_output}/{self.list_vtu[i]}') - self.parallel_run(modify_array_loop, range(len(self.list_vtu)), tqdm_desc = "Assigning array") + self.parallel_run(modify_array_loop, range(len(self.list_pvtu)), tqdm_desc = "Assigning array") diff --git a/contrib/postprocessing/lethe_pyvista_tools/_sort_by_array.py b/contrib/postprocessing/lethe_pyvista_tools/_sort_by_array.py index 44972beac8..de17af8397 100644 --- a/contrib/postprocessing/lethe_pyvista_tools/_sort_by_array.py +++ b/contrib/postprocessing/lethe_pyvista_tools/_sort_by_array.py @@ -28,9 +28,9 @@ def sort_by_array_loop(i): for name in df.array_names: df[name] = df[name][df[reference_array_name].argsort()] - df.save(f'{self.path_output}/{self.list_vtu[i]}') + df.save(f'{self.path_output}/{self.list_vtu[i]}', binary = False) - self.parallel_run(sort_by_array_loop, range(len(self.list_vtu)), + self.parallel_run(sort_by_array_loop, range(len(self.list_pvtu)), tqdm_desc=f"Sorting dataframe by {reference_array_name}") self.sorted = True diff --git a/contrib/postprocessing/lethe_pyvista_tools/_write_df_to_pvtu.py b/contrib/postprocessing/lethe_pyvista_tools/_write_df_to_pvtu.py index f652e2ddf8..31a3de6c6c 100644 --- a/contrib/postprocessing/lethe_pyvista_tools/_write_df_to_pvtu.py +++ b/contrib/postprocessing/lethe_pyvista_tools/_write_df_to_pvtu.py @@ -31,8 +31,8 @@ def write_df_to_pvtu(self, prefix = "mod_"): # If line matches one of the files if path in line: - # If pvtu is in list_vtu - if line.split('file="')[1].split('"/>')[0] in self.list_vtu: + # If pvtu is in list_pvtu + if line.split('file="')[1].split('"/>')[0] in self.list_pvtu: line = line.replace('file="', f'file="{prefix}') pvd_out.write(line) read_files_path_list.remove(path) @@ -48,7 +48,7 @@ def write_df_to_pvtu(self, prefix = "mod_"): N_pvtu = len(self.df) pbar = tqdm(total = N_pvtu, desc="Writing new PVTU and PVD files") for i in range(len(self.df)): - self.df[i].save(f'{self.path_output}/{prefix}{self.list_vtu[i]}') + self.df[i].save(f'{self.path_output}/{prefix}{self.list_vtu[i]}', binary = False) pbar.update(1) print(f"Modified .pvtu and .pvd files with prefix {prefix} successfully written") diff --git a/doc/source/examples/dem/small-scale-rotating-drum-postprocessing/small-scale-rotating-drum-postprocessing.rst b/doc/source/examples/dem/small-scale-rotating-drum-postprocessing/small-scale-rotating-drum-postprocessing.rst index d5e0f88a06..15985c4b2a 100644 --- a/doc/source/examples/dem/small-scale-rotating-drum-postprocessing/small-scale-rotating-drum-postprocessing.rst +++ b/doc/source/examples/dem/small-scale-rotating-drum-postprocessing/small-scale-rotating-drum-postprocessing.rst @@ -12,7 +12,7 @@ This is an example of how to post-process results obtained in the `Small scale r .. warning:: - Details about installing the module or using it without installing it are available on this `documentation <../../../tools/postprocessing/postprocessing.py>`_. + Details about installing the module or using it without installing it are available on `here <../../../tools/postprocessing/postprocessing.py>`_. ---------------------------------- @@ -51,7 +51,7 @@ The DEM files used in this example are obtained following the `Small scale rotat Python Code --------------- -Please, read this `documentation <../../../tools/postprocessing/postprocessing.py>`_ before jumping to the following steps. +Please, read this `documentation <../../../tools/postprocessing/postprocessing_pyvista>`_ before jumping to the following steps. Constructing the Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/examples/dem/small-scale-rotating-drum/small-scale-rotating-drum.rst b/doc/source/examples/dem/small-scale-rotating-drum/small-scale-rotating-drum.rst index 9d9bec908e..13068abd97 100644 --- a/doc/source/examples/dem/small-scale-rotating-drum/small-scale-rotating-drum.rst +++ b/doc/source/examples/dem/small-scale-rotating-drum/small-scale-rotating-drum.rst @@ -38,22 +38,24 @@ Parameter File Mesh ~~~~~ -In this example, we choose a ``cylinder`` grid type to create a cylinder. Grid arguments are the radius of the cylinder (0.056 m) and half-length (0.051 m), respectively. The grid is refined 3 times using the ``set initial refinement`` parameters. The ``expand particle-wall contact search`` is used in concave geometries to enable extended particle-wall contact search with boundary faces of neighbor cells for particles located in each boundary cell (for more details see `Rotating Drum example <../rotating-drum/rotating-drum.html>`_). +In this example, we choose a ``cylinder`` grid type to create a cylinder. Grid arguments are the radius of the cylinder (0.056 m) and half-length (0.051 m), respectively. The grid is refined 3 times using the ``set initial refinement`` parameters. The ``expand particle-wall contact search`` is used in concave geometries to enable extended particle-wall contact search with boundary faces of neighbor cells for particles located in each boundary cell (for more details see `Rotating Drum example <../rotating-drum/rotating-drum.html>`_). The mesh subsection is the same for both parameter files. + .. code-block:: text subsection mesh - set type = dealii - set grid type = cylinder - set grid arguments = 0.056:0.051 - set initial refinement = 3 + set type = dealii + set grid type = cylinder + set grid arguments = 0.056:0.051 + set initial refinement = 3 + set expand particle-wall contact search = true end Packing information ~~~~~~~~~~~~~~~~~~~~ -An insertion box is defined inside the cylindrical domain, inserting 8000 particles every 0.5 seconds while the cylinder is at rest. It is important to note the size of the insertion box to make sure it is completely inside our geometry. Otherwise, particles will be lost during the insertion stage. +An insertion box is defined inside the cylindrical domain, inserting 8000 particles every 0.5 seconds while the cylinder is at rest. It is important to note the size of the insertion box to make sure it is completely inside our geometry to prevent particles loss during the insertion stage. This section is in the ``packing-rotating-drum.prm`` file. .. code-block:: text @@ -68,12 +70,12 @@ An insertion box is defined inside the cylindrical domain, inserting 8000 partic set insertion prn seed = 19 end -Restart files are written once the packing ends. The restart files are used to start the DEM simulation with the imposed rotating boundary condition. +Restart files are written once the packing ends. Using these restart files we can run the rotating drum simulation the end of the packing simulation. Lagrangian Physical Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The particles are mono-dispersed with a radius of 0.0015 m and a density of 2500 kg/m3, respectively. All other particles' physical parameters are taken arbitrary and should be changed based on the physical properties and the experimental values. +The particles are mono-dispersed with a radius of 0.0015 m and a density of 2500 kg/m\ :sup:`3`. All other particles' physical parameters are taken arbitrary and should be changed based on the physical properties and the experimental values. Both parameters files have the same physical properties. .. code-block:: text @@ -95,7 +97,7 @@ The particles are mono-dispersed with a radius of 0.0015 m and a density of 2500 set young modulus wall = 100000000 set poisson ratio wall = 0.24 set restitution coefficient wall = 0.85 - set friction coefficient wall = 0.35 + set friction coefficient wall = 0.3 set rolling friction wall = 0.1 end @@ -103,15 +105,15 @@ The particles are mono-dispersed with a radius of 0.0015 m and a density of 2500 Model Parameters ~~~~~~~~~~~~~~~~~ -In this example, we use the ``dynamic`` load balancing method. This method checks frequently if load balancing should be applied based on a user inputted frequency. Load balancing is dynamically applied if a certain condition is applied. More details regarding load balancing are explained in the `Rotating Drum example <../rotating-drum/rotating-drum.html>`_. +In this example, we use the ``dynamic`` load balancing method. This method checks frequently if load balancing should be applied based on a user inputted frequency. Load balancing is dynamically applied if a certain condition is applied. More details regarding load balancing are explained in the `Rotating Drum example <../rotating-drum/rotating-drum.html>`_. This section is in the ``small-rotating-drum-dem.prm`` file. .. code-block:: text subsection model parameters subsection contact detection set contact detection method = dynamic - set dynamic contact search size coefficient = 0.8 - set neighborhood threshold = 1.3 + set dynamic contact search size coefficient = 0.9 + set neighborhood threshold = 1.2 end subsection load balancing set load balance method = dynamic @@ -127,7 +129,7 @@ In this example, we use the ``dynamic`` load balancing method. This method check DEM Boundary Conditions ~~~~~~~~~~~~~~~~~~~~~~~ -The rotation of the cylinder is applied using a rotational boundary condition with a value of 1 rad/s over the x axis. Based on `deal.II boundary colouring `_, the hull of the cylinder (rotating drum) has an id = 0. +The rotation of the cylinder is applied using a rotational boundary condition with a value of 1 rad/s over the x axis. Based on `deal.II boundary colouring `_, the hull of the cylinder (rotating drum) has an id = 0. This section is in the ``small-rotating-drum-dem.prm`` file. .. code-block:: text @@ -145,28 +147,31 @@ The rotation of the cylinder is applied using a rotational boundary condition wi Simulation Control ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The packing ``lethe-particles`` simulation was run for 2 seconds in real time. +The packing ``lethe-particles`` simulation was run for 2 seconds in real time. This section is in the ``packing-rotating-drum.prm`` file. .. code-block:: text subsection simulation control - set time step = 5e-6 - set time end = 2 - set log frequency = 2000 - set output frequency = 2000 - set output path = ./output_dem/ + set time step = 5e-6 + set time end = 2 + set log frequency = 2000 + set output frequency = 2000 + set output path = ./output_dem/ + set output boundaries = true end -The actual rotation of the drum is 3 seconds in real time. We set the time equal to 5 seconds as the simulation is restarted after the packing ``lethe-particles`` simulation. +The actual rotation of the drum is 3 seconds in real time. We set the time equal to 5 seconds as the simulation is restarted after the packing ``lethe-particles`` simulation. This section is in the ``small-rotating-drum-dem.prm`` file. + .. code-block:: text subsection simulation control - set time step = 5e-6 - set time end = 5 - set log frequency = 2000 - set output frequency = 2000 - set output path = ./output_dem/ + set time step = 5e-6 + set time end = 5 + set log frequency = 2000 + set output frequency = 2000 + set output path = ./output_dem/ + set output boundaries = true end diff --git a/examples/dem/3d-small-scale-rotating-drum-postprocessing/3d-example_small_rotating_drum.py b/examples/dem/3d-small-scale-rotating-drum-postprocessing/3d-example_small_rotating_drum.py index 3e0f9315c9..5a0b25f3f1 100644 --- a/examples/dem/3d-small-scale-rotating-drum-postprocessing/3d-example_small_rotating_drum.py +++ b/examples/dem/3d-small-scale-rotating-drum-postprocessing/3d-example_small_rotating_drum.py @@ -12,12 +12,12 @@ # Import tools from module from lethe_pyvista_tools import * +import numpy as np import matplotlib.pyplot as plt # Create object named "particles" -particles = lethe_pyvista_tools(".", "small-rotating-drum-dem.prm", "out.pvd") - +particles = lethe_pyvista_tools(".", "small-rotating-drum-dem.prm", "out.pvd", prefix="mod_", first = 200) # State condition for particle_color array creation condition = "(y**2 + z**2)**(1/2) > 0.025"