Mass flow rate across boundaries

May I know the most elegant way to integrate mass flux across a boundary to obtain the mass flow rate? I would like to track this value at some given time-steps, similar to how the forces/moments are tracked.

There is currently no direct means in PyFR of accomplishing this. However, it should not be difficult to add; the force integrator plugin contains all of the required mechanics for integrating quantities over boundaries. Thus with a few minor modifications it can be used to integrate mass.

Regards, Freddie.

Thank you. Does something like the following look like a step in the right direction?

from collections import defaultdict

import numpy as np

from pyfr.mpiutil import get_comm_rank_root, mpi
from pyfr.plugins.base import BaseSolnPlugin, SurfaceMixin, init_csv


class FluidFluxPlugin(SurfaceMixin, BaseSolnPlugin):
    name = 'fluidflux'
    systems = ['ac-euler', 'ac-navier-stokes', 'euler', 'navier-stokes']
    formulations = ['dual', 'std']
    dimensions = [2, 3]

    def __init__(self, intg, cfgsect, suffix):
        super().__init__(intg, cfgsect, suffix)

        comm, rank, root = get_comm_rank_root()

        # Output frequency
        self.nsteps = self.cfg.getint(cfgsect, 'nsteps')

        # Check if the system is incompressible
        self._ac = intg.system.name.startswith('ac')

        # Viscous correction
        self._viscorr = self.cfg.get('solver', 'viscosity-correction', 'none')

        # Constant variables
        self._constants = self.cfg.items_as('constants', float)

        # Underlying elements class
        self.elementscls = intg.system.elementscls

        # Boundary to integrate over
        bc = f'bcon_{suffix}_p{intg.rallocs.prank}'

        # Get the mesh and elements
        mesh, elemap = intg.system.mesh, intg.system.ele_map

        # See which ranks have the boundary
        bcranks = comm.gather(bc in mesh, root=root)

        # The root rank needs to open the output file
        if rank == root:
            if not any(bcranks):
                raise RuntimeError(f'Boundary {suffix} does not exist')

            # CSV header
            header = ['t', 'massflux'][:2]

            # Open
            self.outf = init_csv(self.cfg, cfgsect, ','.join(header))

        # Interpolation matrices and quadrature weights
        self._m0 = m0 = {}
        self._qwts = qwts = defaultdict(list)

        # If we have the boundary then process the interface
        if bc in mesh:
            # Element indices and associated face normals
            eidxs = defaultdict(list)
            norms = defaultdict(list)

            for etype, eidx, fidx, flags in mesh[bc].tolist():
                eles = elemap[etype]
                itype, proj, norm = eles.basis.faces[fidx]

                ppts, pwts = self._surf_quad(itype, proj, flags='s')
                nppts = len(ppts)

                # Get phyical normals
                pnorm = eles.pnorm_at(ppts, [norm]*nppts)[:, eidx]

                eidxs[etype, fidx].append(eidx)
                norms[etype, fidx].append(pnorm)

                if (etype, fidx) not in m0:
                    m0[etype, fidx] = eles.basis.ubasis.nodal_basis_at(ppts)
                    qwts[etype, fidx] = pwts

            self._eidxs = {k: np.array(v) for k, v in eidxs.items()}
            self._norms = {k: np.array(v) for k, v in norms.items()}

    def __call__(self, intg):
        # Return if no output is due
        if intg.nacptsteps % self.nsteps:
            return

        # MPI info
        comm, rank, root = get_comm_rank_root()

        # Solution matrices indexed by element type
        solns = dict(zip(intg.system.ele_types, intg.soln))
        nvars = self.nvars
        # Flux
        f = np.zeros(1)

        for etype, fidx in self._m0:
            # Get the interpolation operator
            m0 = self._m0[etype, fidx]
            nfpts, nupts = m0.shape

            # Extract the relevant elements from the solution
            uupts = solns[etype][..., self._eidxs[etype, fidx]]

            # Interpolate to the face
            ufpts = m0 @ uupts.reshape(nupts, -1)
            ufpts = ufpts.reshape(nfpts, nvars, -1)
            ufpts = ufpts.swapaxes(0, 1)

            # Compute the velocity vector
            rho = 1.0 if self._ac else self.elementscls.con_to_pri(ufpts, self.cfg)[0]
            vs = np.array(self.elementscls.con_to_pri(ufpts, self.cfg)[1:-1])

            # Get the quadrature weights and normal vectors
            qwts = self._qwts[etype, fidx]
            norms = self._norms[etype, fidx]

            # Do the quadrature
            f[0] += np.einsum('i,im,jim,dik->',qwts, rho, vs, norms)

        # Reduce and output if we're the root rank
        if rank != root:
            comm.Reduce(f, None, op=mpi.SUM, root=root)
        else:
            comm.Reduce(mpi.IN_PLACE, f, op=mpi.SUM, root=root)

            # Write
            print(intg.tcurr, *f.ravel(), sep=',', file=self.outf)

            # Flush to disk
            self.outf.flush()

Yes, although you’ll always want to double check the einsum expression as they can be very tricky. It might be easier just to do rho*vs and pass that into einsum (which will dot with the normal and do the quadrature).

Regards, Freddie.

Thanks. There was some issue with the previous code but the following should work:

from collections import defaultdict

import numpy as np

from pyfr.mpiutil import get_comm_rank_root, mpi
from pyfr.plugins.base import BaseSolnPlugin, SurfaceMixin, init_csv


class FluidFluxPlugin(SurfaceMixin, BaseSolnPlugin):
    name = 'fluidflux'
    systems = ['ac-euler', 'ac-navier-stokes', 'euler', 'navier-stokes']
    formulations = ['dual', 'std']
    dimensions = [2, 3]

    def __init__(self, intg, cfgsect, suffix):
        super().__init__(intg, cfgsect, suffix)

        comm, rank, root = get_comm_rank_root()

        # Output frequency
        self.nsteps = self.cfg.getint(cfgsect, 'nsteps')

        # Check if the system is incompressible
        self._ac = intg.system.name.startswith('ac')

        # Viscous correction
        self._viscorr = self.cfg.get('solver', 'viscosity-correction', 'none')

        # Constant variables
        self._constants = self.cfg.items_as('constants', float)

        # Underlying elements class
        self.elementscls = intg.system.elementscls

        # Boundary to integrate over
        bc = f'bcon_{suffix}_p{intg.rallocs.prank}'

        # Get the mesh and elements
        mesh, elemap = intg.system.mesh, intg.system.ele_map

        # See which ranks have the boundary
        bcranks = comm.gather(bc in mesh, root=root)

        # The root rank needs to open the output file
        if rank == root:
            if not any(bcranks):
                raise RuntimeError(f'Boundary {suffix} does not exist')

            # CSV header
            header = ['t', 'massflux'][:2]

            # Open
            self.outf = init_csv(self.cfg, cfgsect, ','.join(header))

        # Interpolation matrices and quadrature weights
        self._m0 = m0 = {}
        self._qwts = qwts = defaultdict(list)

        # If we have the boundary then process the interface
        if bc in mesh:
            # Element indices and associated face normals
            eidxs = defaultdict(list)
            norms = defaultdict(list)

            for etype, eidx, fidx, flags in mesh[bc].tolist():
                eles = elemap[etype]
                itype, proj, norm = eles.basis.faces[fidx]

                ppts, pwts = self._surf_quad(itype, proj, flags='s')
                nppts = len(ppts)

                # Get phyical normals
                pnorm = eles.pnorm_at(ppts, [norm]*nppts)[:, eidx]

                eidxs[etype, fidx].append(eidx)
                norms[etype, fidx].append(pnorm)

                if (etype, fidx) not in m0:
                    m0[etype, fidx] = eles.basis.ubasis.nodal_basis_at(ppts)
                    qwts[etype, fidx] = pwts

            self._eidxs = {k: np.array(v) for k, v in eidxs.items()}
            self._norms = {k: np.array(v) for k, v in norms.items()}

    def __call__(self, intg):
        # Return if no output is due
        if intg.nacptsteps % self.nsteps:
            return

        # MPI info
        comm, rank, root = get_comm_rank_root()

        # Solution matrices indexed by element type
        solns = dict(zip(intg.system.ele_types, intg.soln))
        nvars = self.nvars
        # Flux
        f = np.zeros(1)

        for etype, fidx in self._m0:
            # Get the interpolation operator
            m0 = self._m0[etype, fidx]
            nfpts, nupts = m0.shape

            # Extract the relevant elements from the solution
            uupts = solns[etype][..., self._eidxs[etype, fidx]]

            # Interpolate to the face
            ufpts = m0 @ uupts.reshape(nupts, -1)
            ufpts = ufpts.reshape(nfpts, nvars, -1)
            ufpts = ufpts.swapaxes(0, 1)

            # Convert conservative variables to primitive variables
            pri_vars = self.elementscls.con_to_pri(ufpts, self.cfg)
            vs = np.array(pri_vars[1:-1])  # Velocity components

            if self._ac:
                # Artificial compressibility: density is not a primitive variable
                # Set rho to 1 with the appropriate shape
                rho = np.ones_like(vs[0])
            else:
                # Compressible flow: extract rho from primitive variables
                rho = pri_vars[0]

            # Compute rhovs: multiply rho and vs
            rhovs = rho[None, :, :] * vs

            # Get the quadrature weights and normal vectors
            qwts = self._qwts[etype, fidx]
            norms = self._norms[etype, fidx]
            # Do the quadrature
            f[0] += np.einsum('i,jim,mij->', qwts, rhovs, norms)

        # Reduce and output if we're the root rank
        if rank != root:
            comm.Reduce(f, None, op=mpi.SUM, root=root)
        else:
            comm.Reduce(mpi.IN_PLACE, f, op=mpi.SUM, root=root)

            # Write
            print(intg.tcurr, *f.ravel(), sep=',', file=self.outf)

            # Flush to disk
            self.outf.flush()

This seems to work on my simple cases but I am facing some indirectly related issues (like for instance I am not able to maintain a mass flow rate through a boundary with an imposed velocity, perhaps due to weakly-imposed boundary conditions?). I will keep testing it further and do a pull request if I get the chance.

It is not always possible to maintain a mass flow rate through a boundary. Consider a channel with a high-subsonic inflow. Here, you should have no trouble getting the expected mass-flow rate at the boundary. Now, consider putting a body (such as a aerofoil) in the channel. As the flow approaches the aerofoil it will choke which, in turn, will limit the amount of mass which can enter the domain upstream. The mass flow rate will therefore be less than you expect.

Regards, Freddie.