bounds (Photo Robert Katzki on Unsplash) Setting the boundaries is setting the game, even for large eddy simulations.

The solutBound file manages the boundaries of the domain for AVBP. PyAVBP provides many ways to create or edit a solutbound.

Example of solutbound

Create any Solutbound from scratch

There is a crude helper function in pyavbp called gensolutbound(). It generates AVBP solutBound.h5 files without any check using a blueprint. Better start with an example:

from pyavbp.io.mesh_utils import load_mesh_bnd
from pyavbp.io.gensolutbound import gensolutbound 
bnd_data  = load_mesh_bnd('./trappedvtx_perio.mesh.h5')
bnd_targets =  [
    { 
        "fields": {
            "RhoUn": {
                "type": "constant",
                "value": 2.,
            },
            "Urms": {
                "type": "constant",
                "value": 100.,
            },
            "mixture": {
                "type": "constant",
                "value": [ 0.75, 0.25],
            },
        },
        "name" : "Inlet",  
    },
    { 
        "name" : "Outlet",
        "fields": {
            "Pressure": {
                "type": "constant",
                "value": 101325.,
            },
        },
    },
]
mixture_species = ["O2", "N2"]
gensolutbound(bnd_data, bnd_targets, mixture_species, "raw_solutbound.h5")

This small script creates a solutBound for the mesh trappedvtx_perio. Showing the data inside, we get:

>h5cross stats raw_solutbound.h5 
+----------------------------+----------+----------+----------+--------+----------+
| Dataset                    |   min    |   mean   |   max    | st dev |  median  |
+----------------------------+----------+----------+----------+--------+----------+
| /Patch_001-Inlet/N2        |   0.25   |   0.25   |   0.25   |  0.0   |   0.25   |
| /Patch_001-Inlet/O2        |   0.75   |   0.75   |   0.75   |  0.0   |   0.75   |
| /Patch_001-Inlet/RhoUn     |   2.0    |   2.0    |   2.0    |  0.0   |   2.0    |
| /Patch_001-Inlet/Urms      |  100.0   |  100.0   |  100.0   |  0.0   |  100.0   |
| /Patch_002-Outlet/Pressure | 101325.0 | 101325.0 | 101325.0 |  0.0   | 101325.0 |
+----------------------------+----------+----------+----------+--------+----------+
Custom fields

However you can generate any kind of custom field, as long a you provide a numpy array of the correct shape. In this case, the inlet patch is defined by 417 vertices. Here we replace replace the value 2. of RhoUn by 2.*np.random.rand(417) -1.:

import numpy as np
(...)
bnd_targets =  [
    { 
        "fields": {
            "RhoUn": {
                "type": "constant",
                "value": 2.*np.random.rand(417) -1.,
            },
(...)

We get the same solutBound with a random field:

+----------------------------+-------------+-------------+------------+------------+-------------+
| Dataset                    |     min     |     mean    |    max     |   st dev   |    median   |
+----------------------------+-------------+-------------+------------+------------+-------------+
| /Patch_001-Inlet/N2        |     0.25    |     0.25    |    0.25    |    0.0     |     0.25    |
| /Patch_001-Inlet/O2        |     0.75    |     0.75    |    0.75    |    0.0     |     0.75    |
| /Patch_001-Inlet/RhoUn     | -0.99816073 | -0.01738461 | 0.98975535 | 0.57305945 | -0.02638937 |
| /Patch_001-Inlet/Urms      |    100.0    |    100.0    |   100.0    |    0.0     |    100.0    |
| /Patch_002-Outlet/Pressure |   101325.0  |   101325.0  |  101325.0  |    0.0     |   101325.0  |
+----------------------------+-------------+-------------+------------+------------+-------------+

So fields can be either a single scalar or a numpy array of the correct shape.

What if I mess up ?

If you mess up the shape, you will get the following error:

Traceback (most recent call last):
  File "/Users/dauptain/PYAVBP/GENPROFILE/trial_genprofile.py", line 94, in <module>
    gensolutbound(bnd_data, bnd_targets, mixture_species, "raw_solutbound.h5")
  File "/Users/dauptain/GITLAB/pyavbp/src/pyavbp/io/gensolutbound.py", line 131, in gensolutbound
    boundary_target[bnd_p["name"]] = _create_boundary_data(
  File "/Users/dauptain/GITLAB/pyavbp/src/pyavbp/io/gensolutbound.py", line 91, in _create_boundary_data
    out[field] = scalar_array * bnd_fields[field]["value"]
ValueError: operands could not be broadcast together with shapes (417,) (418,) 

If you provide a pathname not present in the mesh, you will get the following error:

Traceback (most recent call last):
  File "/Users/dauptain/PYAVBP/GENPROFILE/trial_genprofile.py", line 94, in <module>
    gensolutbound(bnd_data, bnd_targets, mixture_species, "raw_solutbound.h5")
  File "/Users/dauptain/GITLAB/pyavbp/src/pyavbp/io/gensolutbound.py", line 133, in gensolutbound
    patch_data[bnd_p["name"]],
KeyError: 'InletXXX'
Takeaway

In a nutshell this gensolutbound() is only writing down to the disc what is in the blueprint bnd_target. All complex treatments (makeinject, genprofile, patchmapper) must be done before, in a non-AVBP, pure Numpy context.

Edit an existing solutBound

Add a hot spot

Here we want to add a hot spot at the inlet of the domain. We will directly edit the h5 file. There are plenty of Python packages to edit HDF files, some of them are described in this post. We choose to use h5py and hdfdict.

import h5py
import hdfdict
from pyavbp.io.h5_to_xmf_solutbound import write_xmf

We use with instructions to make sure the files are closed at the end of the script. h5py is used to read the original file while hdfdict is used to transform the file into a dict and to render it easier to edit. We also use PyAVBP’s write_xmf function.

with h5py.File('./oms_combu.solutbound.h5','r') as fin:
    solutbound_dict=hdfdict.load(fin)
    field=solutbound_dict['Patch_001-Inlet']['Temperature']
    solutbound_dict['Patch_001-Inlet']['Temperature'][100]=1000
    solutbound_dict['Patch_001-Inlet']['Temperature'][250]=1000
    with h5py.File('new_solutbound.h5','w') as fout:
        hdfdict.dump(solutbound_dict,fout)
write_xmf(meshfile, 'new_solutbound.h5', 'new_solutbound.xmf')

Two hot spots

Apply a profile based on coordinates

In this part, we will change a patch property according to the coordinates. For example, we want to apply a constant sinus-shaped temperature at the inlet.

# Some imports

We start importing the same packages as before plus a package to get mesh information and numpy:

import h5py
from h5cross import hdfdict
from pyavbp.io.mesh_utils import load_mesh_bnd,get_mesh_bulk
from pyavbp.io.h5_to_xmf_solutbound import write_xmf
import numpy as np
# Mesh manipulations

We use mesh_utils to get the coordinates of the points at the inlet patch:

solutbound_file = "./oms_combu.solutbound.h5"
meshfile = "../../COMMON/oms_combu.mesh.h5"
mesh = get_mesh_bulk(meshfile)
coord = load_mesh_bnd(meshfile)['Inlet']['xyz']
# Dict instantiation

Then, we use h5py to read the current solutbound and put it in a dict with hdfdict.

with h5py.File(solutbound_file,'r') as fin:
    datadict = hdfdict.load(fin)

We create a new array with the temperatures we want:

    new_temp = 300 + 20 * np.sin(coord[:,2]*50*2*np.pi)
    datadict['Patch_001-Inlet']['Temperature'] = new_temp
# Dumping the solution

We conclude by writing the new values in the dict and writing a new solutbound file:

    with h5py.File('new_solutbound.h5','w') as fout:
        hdfdict.dump(datadict,fout)
    write_xmf(meshfile, 'new_solutbound.h5', 'new_solutbound.xmf')

New solutbound

Apply a profile created with patchmapper

Disclaimer: This option is available only for patches with x constant profiles

Patchmapper is a tool that applies a profile defined as a numpy.array to an axisymmetric patch. You have to give it a dict with all the profiles you want to change and a reference array with key name ['rR'] along the height to make the interpolation. Make sure the lengths of the arrays in the dict are all the same, but not necessarily the same as the number of nodes in the patch.

# Some importations

We import the same packages as before for handling the HDF files, plus the package containing patchmapper and a package for mesh manipulation:

import h5py
import hdfdict
from pyavbp.tools import patchmapper as pm
from pyavbp.io.mesh_utils import load_mesh_bnd
from pyavbp.io.h5_to_xmf_solutbound import write_xmf
mport numpy as np
# Mesh and solutbound handling

Now, we load the mesh and the solutbound files. We store the coordinates of the nodes for later:

solutbound_file = "./oms_combu.solutbound.h5"
meshfile = "../../COMMON/oms_combu.mesh.h5"
coord = load_mesh_bnd(meshfile)['Inlet']['xyz']

The mesh

# Dict creation

We create the dict that we will use as an input of patchmapper. First, we calculate the radial coordinates of all the points of the mesh in order to find the extrema and to create the ‘rR’ entry of the dict for the interpolation made by patchmapper.

with h5py.File(solutbound_file,'r') as fin:
    datadict = hdfdict.load(fin)
    radial_points=2000
    radial_coord = np.hypot(coord[:,1],coord[:,2])
    profile_dict=dict()
    profile_dict['rR'] = np.linspace(np.min(radial_coord),
                                     np.max(radial_coord),
                                     radial_points)

Now, we can create all the entries we want in the dict. They have to be defined as vectors that will be interpolated on the mesh given the ‘rR’ vector. In this example, we create a temperature vector with a 1000K band and 600K everywhere else. The length of the hot band is one third of the total height located in the middle of the inlet patch:

    temp = 600 * np.ones(radial_points)
    temp[int(len(temp) / 3):int(len(temp) * 2 / 3)] = 1000
    profile_dict['Temperature'] = temp
# Patchmapper and dumping the solution

We call the patchmapper function with the right inputs. We change the initial dict with the new values. Then, we dump the new solutBound and the xmf file:

    new_fields_inlet = pm.apply_patchmapper_1d(profile_dict, coord)
    datadict['Patch_001-Inlet']['Temperature'] = new_fields_inlet['Temperature']
    with h5py.File('new_solutbound.h5','w') as fout:
        hdfdict.dump(datadict,fout)
    write_xmf(meshfile, 'new_solutbound.h5', 'new_solutbound.xmf')

Patchmapper defined profile

# The full code
import h5py
from h5cross import hdfdict
from pyavbp.tools import patchmapper as pm
from pyavbp.io.mesh_utils import load_mesh_bnd
from pyavbp.io.h5_to_xmf_solutbound import write_xmf

import numpy as np

#Mesh and solutboud handling
solutbound_file = "./oms_combu.solutbound.h5"
meshfile = "../../COMMON/oms_combu.mesh.h5"
coord = load_mesh_bnd(meshfile)['Inlet']['xyz']

#Reading the initial solutbound and initializing the output dict
with h5py.File(solutbound_file,'r') as fin:
    datadict = hdfdict.load(fin)
    radial_points=2000
    radial_coord = np.hypot(coord[:,1],coord[:,2])
    profile_dict=dict()
    profile_dict['rR'] = np.linspace(np.min(radial_coord),
                                     np.max(radial_coord),
                                     radial_points)

    #Creating temperature vector and applying patchmapper
    temp = 600 * np.ones(radial_points)
    temp[int(len(temp) / 3):int(len(temp) * 2 / 3)] = 1000
    profile_dict['Temperature'] = temp
    new_fields_inlet = pm.apply_patchmapper_1d(profile_dict, coord)
    datadict['Patch_001-Inlet']['Temperature'] = new_fields_inlet['Temperature']

    #Writing the new values in a new file
    with h5py.File('new_solutbound.h5','w') as fout:
        hdfdict.dump(datadict,fout)
    write_xmf(meshfile, 'new_solutbound.h5', 'new_solutbound.xmf')   

Generalization with a CSV data to map on a patch

Here is a more advanced example where the origin data is not a profile but a point cloud provided in a CSV file. Copy and paste what you need…

Advanced example

import numpy as np
import matplotlib.pyplot as plt
import h5py
from h5cross import hdfdict
from pyavbp.io.mesh_utils import load_mesh_bnd
from pyavbp.io.h5_to_xmf_solutbound import write_xmf
from cloud2cloud import cloud2cloud
from arnica.utils.vector_actions import rotate_vect_around_axis
from arnica.utils.nparray2xmf import NpArray2Xmf

SIZE = 1000


def gen_fake_data():
    """Generate fake data if you do not have a CSV file for your tests"""
    rads = np.random.random(size=SIZE) * 0.1
    thetas = np.random.random(size=SIZE) * 2 * np.pi
    x = rads * np.cos(thetas)
    y = rads * np.sin(thetas)
    plt.scatter(x, y, c=y)
    plt.show()
    sce_coords = np.stack([x, y, 0 * x], axis=1)
    sce_data = np.stack([r, y], axis=1)
    return sce_coords, sce_data


def load_csv_file(filename):
    """Reading a csv file with only values

    We assume here thar the 3 first columns are X, Y and Z"""
    # Open and read the CSV file
    data = np.loadtxt(filename, delimiter=",")
    sce_coords = data[:, :3]
    # rotation de 90 degres autour de Y
    sce_coords = rotate_vect_around_axis(sce_coords, ([0, 1, 0], 90.0))
    # scaling pto fit on  trapped vortex mesh geometry

    sce_coords[:, 0] = sce_coords[:, 0] - sce_coords[:, 0].mean()
    sce_coords[:, 1] = sce_coords[:, 1] - sce_coords[:, 1].mean()
    sce_coords[:, 1] = sce_coords[:, 1] / sce_coords[:, 1].max() * 0.13
    sce_coords[:, 2] = sce_coords[:, 2] - sce_coords[:, 2].mean()
    sce_coords[:, 2] = sce_coords[:, 2] / sce_coords[:, 2].max() * 0.13

    sce_data = data[:, 3:]

    # plt.scatter(sce_coords[:,1],sce_coords[:,2],c=sce_data[:,-1])
    # plt.show()

    return sce_coords, sce_data


def projection():
    """Example of a solutbound where we change some fields and add other fields"""
    # Read source data
    sce_coords, sce_data = load_csv_file(csv_file)
    # A way to dump the data before interpolation
    dout = NpArray2Xmf("data.h5")
    dout.create_grid(sce_coords[:, 0], sce_coords[:, 1], sce_coords[:, 2])
    for i in range(6):
        dout.add_field(sce_data[:, i], "var_" + str(i))
    dout.dump()

    # load mesh data
    solutbound_file = "./oms_combu.solutbound.h5"
    meshfile = "./trappedvtx_perio.mesh.h5"
    csv_file = "FRONT_order.csv"
    coord = load_mesh_bnd(meshfile)["Inlet"]["xyz"]

    # projection
    projection = cloud2cloud(sce_coords, sce_data, coord, stencil=4)

    # editing the HDF5 file
    with h5py.File(solutbound_file, "r") as fin:
        datadict = hdfdict.load(fin)

        datadict["Patch_001-Inlet"]["var_0"] = projection[:, 0]
        datadict["Patch_001-Inlet"]["var_1"] = projection[:, 1]
        datadict["Patch_001-Inlet"]["var_2"] = projection[:, 2]
        datadict["Patch_001-Inlet"]["Pression"] = projection[:, 3]
        datadict["Patch_001-Inlet"]["Temperature"] = projection[:, 4]
        datadict["Patch_001-Inlet"]["var_4"] = projection[:, 5]

        # Writing the new values in a new file
        with h5py.File("new_solutbound.h5", "w") as fout:
            hdfdict.dump(datadict, fout)
        write_xmf(meshfile, "new_solutbound.h5", "new_solutbound.xmf")


projection()

Like this post? Share on: TwitterFacebookEmail


Matthieu Rossi is an engineer focused on making COOP tools available to industry.
Antoine Dauptain is a research scientist focused on computer science and engineering topics for HPC.

Keep Reading


Published

Category

Tutorials

Tags

Stay in Touch