Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1deecfe
Add SwirlMixer class
malihass Jul 29, 2026
b9d3df2
rename to actuator class, swirl is decided later
malihass Jul 29, 2026
13ad8c3
ball-source fvModels writer
malihass Jul 29, 2026
8a71204
format
malihass Jul 29, 2026
7f8e3e5
machinery to call the right mixer
malihass Jul 29, 2026
b6681fd
new tutorial for the new mixer
malihass Jul 29, 2026
69652ee
add actuator mixer test
malihass Jul 29, 2026
19eed37
actuator-disk power helper and mixer-model keys for case generation
malihass Jul 30, 2026
699bb0a
improve design sweep gen
malihass Jul 30, 2026
a5f9e89
adjust path
malihass Jul 30, 2026
0963bc1
remove te default qos high option
malihass Jul 30, 2026
6f47fe0
fix _readOFScal NameError, we have a public functions for that
malihass Jul 30, 2026
b096c07
update template file with the right qoi
malihass Jul 31, 2026
a15f682
add static mixer class
malihass Aug 3, 2026
fbdd061
add write_static_mixer_ball to the static mixer class
malihass Aug 3, 2026
37c330e
make sure we can mix static and dynamic mixers
malihass Aug 3, 2026
77be001
Add template for mixer placement
malihass Aug 3, 2026
8b3e5b1
doc updates for static/dynamic mixer distinction
malihass Aug 3, 2026
704ff34
sign printing was messed up
malihass Aug 3, 2026
7b1d084
add regression test for later
malihass Aug 3, 2026
e5f1deb
sign issue
malihass Aug 7, 2026
8d596d7
source is -=
malihass Aug 18, 2026
4024b10
regen fvModels
malihass Aug 18, 2026
6525f17
add per level timestepping option
malihass Aug 19, 2026
2253842
robust template folder resolution
malihass Aug 19, 2026
d14d789
make sure outlets are read from template json
malihass Aug 20, 2026
da321fc
use random sampling or shared pool depending on if we need to compute…
malihass Aug 21, 2026
2a5ca8f
also pack post proc
malihass Aug 21, 2026
0e70925
pack post proc together
malihass Aug 21, 2026
bdf264f
add uloop validation case
malihass Sep 1, 2026
f9b1f62
format
malihass Sep 1, 2026
06fa77f
merge main to static mix
malihass Sep 1, 2026
d338975
missing gmsh
malihass Sep 1, 2026
b8535db
update pixi lock
malihass Sep 1, 2026
cc91f70
fix lib hdf5 link
malihass Sep 1, 2026
fe1ee1c
fix hdf5 lib
malihass Sep 2, 2026
eb5d46d
fix application paths and deploy to pypi
malihass Sep 2, 2026
1064f7b
remove run all and adjust allclean
malihass Sep 2, 2026
4e8868f
add data to reproduce plots
malihass Sep 10, 2026
824a222
Add mesh-agnostic loop velocity post-processing
malihass Sep 10, 2026
bf0a38d
Add compute_loop_velocity CLI wrapper
malihass Sep 10, 2026
1d1e9e5
Add steady_stat windowed
malihass Sep 10, 2026
02b40f1
Add interfacial area, turbulent diffusivity, and kL post quantities
malihass Sep 10, 2026
6fd4167
Froude, Weber, Sherwood dimensionless-number helpers
malihass Sep 10, 2026
8000938
Add dimensionless numbers calc
malihass Sep 11, 2026
6c55e3b
needed for reproducing the loop res
malihass Sep 11, 2026
a28d9e1
Merge branch 'main' into static_mix
malihass Sep 11, 2026
8d56542
format
malihass Sep 11, 2026
09fd47e
fix units for dimensionless sherwood
malihass Sep 11, 2026
a9cd43e
safe globalvar read
malihass Sep 11, 2026
3fa0857
Robust stats
malihass Sep 11, 2026
b93239a
fix docstrings
malihass Sep 11, 2026
3665021
format
malihass Sep 11, 2026
986bdbc
new v
malihass Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions applications/compute_loop_velocity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import argparse
import csv
import json
import os

from bird import logger
from bird.postprocess.post_quantities import (
build_loop_direction_field,
build_loop_direction_field_from_path,
compute_loop_velocity,
propose_loop_boxes_block_rect,
)
from bird.utilities.ofio import get_case_times, read_cell_centers


def build_direction_field(args, cell_centers):
"""Loop-direction field from the box, path, or block-rect proposer source."""
if args.boxes is not None:
with open(args.boxes) as f:
boxes = json.load(f)
return build_loop_direction_field(cell_centers, boxes)
if args.path is not None:
with open(args.path) as f:
spec = json.load(f)
return build_loop_direction_field_from_path(
cell_centers, spec["points"], spec["max_dist"]
)
with open(args.mesh) as f:
geometry = json.load(f)["Geometry"]
boxes = propose_loop_boxes_block_rect(geometry, rescale=args.rescale)
return build_loop_direction_field(cell_centers, boxes)


def main():
parser = argparse.ArgumentParser(description="Loop (circulation) velocity")
parser.add_argument(
"-c", "--case", type=str, default=".", help="case folder"
)
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument(
"-b", "--boxes", type=str, help="JSON list of {min, max, direction}"
)
source.add_argument(
"-p",
"--path",
type=str,
help='JSON {"points": [[x,y,z], ...], "max_dist": float}',
)
source.add_argument(
"-m", "--mesh", type=str, help="mesh.json for the block-rect proposer"
)
parser.add_argument(
"-r",
"--rescale",
type=float,
default=None,
help="rescale factor for the proposer (default 1.0)",
)
parser.add_argument(
"-o",
"--output",
type=str,
default=None,
help="output CSV (default <case>/Uliq_loop.csv)",
)
args = parser.parse_args()

cell_centers, _ = read_cell_centers(args.case)
direction_field = build_direction_field(args, cell_centers)

time_values, time_names = get_case_times(args.case, remove_zero=True)
output = args.output or os.path.join(args.case, "Uliq_loop.csv")
with open(output, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["time", "U_loop"])
for time_value, time_name in zip(time_values, time_names):
# fresh cache per time (the caller must reset it)
u_loop, _ = compute_loop_velocity(
args.case, time_name, direction_field, field_dict={}
)
writer.writerow([time_value, u_loop])
logger.info(f"t={time_value:.4g}: U_loop={u_loop:.6g}")
logger.info(f"Wrote {output}")


if __name__ == "__main__":
main()
46 changes: 44 additions & 2 deletions bird/postprocess/post_quantities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,59 @@
_get_ind_slice,
_weighted_average,
)
from .kla import compute_fitted_kla, compute_instantaneous_kla
from .phase import compute_ave_bubble_diam, compute_gas_holdup
from .dimensionless import (
compute_froude_number,
compute_sherwood_number,
compute_weber_number,
froude,
sherwood,
weber,
)
from .kla import (
compute_fitted_kl,
compute_fitted_kla,
compute_instantaneous_kl,
compute_instantaneous_kla,
)
from .loop_velocity import (
build_loop_direction_field,
build_loop_direction_field_from_path,
compute_loop_velocity,
propose_loop_boxes_block_rect,
)
from .phase import (
compute_ave_bubble_diam,
compute_ave_liquid_density,
compute_ave_liquid_velocity,
compute_gas_holdup,
interfacial_area,
)
from .species import compute_ave_conc_liq, compute_ave_y_liq
from .superficial_velocity import compute_superficial_gas_velocity
from .transport import compute_turbulent_diffusivity

__all__ = [
"build_loop_direction_field",
"build_loop_direction_field_from_path",
"compute_ave_bubble_diam",
"compute_ave_conc_liq",
"compute_ave_liquid_density",
"compute_ave_liquid_velocity",
"compute_ave_y_liq",
"compute_fitted_kl",
"compute_fitted_kla",
"compute_froude_number",
"compute_gas_holdup",
"compute_instantaneous_kl",
"compute_instantaneous_kla",
"compute_loop_velocity",
"compute_sherwood_number",
"compute_superficial_gas_velocity",
"compute_turbulent_diffusivity",
"compute_weber_number",
"froude",
"interfacial_area",
"propose_loop_boxes_block_rect",
"sherwood",
"weber",
]
235 changes: 235 additions & 0 deletions bird/postprocess/post_quantities/dimensionless.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import numpy as np

from bird.utilities.ofio import (
read_global_vars,
read_gravity,
read_surface_tension,
)

from .kla import compute_instantaneous_kl
from .phase import compute_ave_liquid_density, compute_ave_liquid_velocity


def froude(velocity: float, length: float, gravity: float = 9.81) -> float:
r"""Froude number :math:`Fr = U / \sqrt{g\,L}`.

Parameters
----------
velocity: float
Characteristic velocity :math:`U`, in :math:`m.s^{-1}`
length: float
Characteristic length :math:`L`, in :math:`m`
gravity: float
Gravitational acceleration :math:`g`, in :math:`m.s^{-2}`

Returns
----------
froude_number: float
Froude number (dimensionless)
"""
return velocity / np.sqrt(gravity * length)


def weber(
density: float, velocity: float, length: float, surface_tension: float
) -> float:
r"""Weber number :math:`We = \rho\,U^2\,L / \sigma`.

Parameters
----------
density: float
Fluid density :math:`\rho`, in :math:`kg.m^{-3}`
velocity: float
Characteristic velocity :math:`U`, in :math:`m.s^{-1}`
length: float
Characteristic length :math:`L`, in :math:`m`
surface_tension: float
Surface tension :math:`\sigma`, in :math:`N.m^{-1}`

Returns
----------
weber_number: float
Weber number (dimensionless)
"""
return density * velocity**2 * length / surface_tension


def sherwood(kl: float, length: float, diffusivity: float) -> float:
r"""Sherwood number :math:`Sh = k_L\,L / D`.

Parameters
----------
kl: float
Mass-transfer coefficient :math:`k_L`, in :math:`m.s^{-1}`
length: float
Characteristic length :math:`L`, in :math:`m`
diffusivity: float
Mass diffusivity :math:`D`, in :math:`m^2.s^{-1}`

Returns
----------
sherwood_number: float
Sherwood number (dimensionless)
"""
return kl * length / diffusivity


def compute_froude_number(
case_folder: str,
time_folder: str,
length: float,
n_cells: int | None = None,
volume_time: str | None = None,
field_dict: dict | None = None,
) -> tuple[float, dict]:
r"""Froude number from the liquid-averaged velocity and a given length.

:math:`Fr = U / \sqrt{g L}` with :math:`U` the liquid-volume-averaged
:math:`|U_{\rm liq}|` at ``time_folder``, :math:`g` from ``constant/g``, and
:math:`L` the passed length.

Parameters
----------
case_folder: str
Path to case folder
time_folder: str
Name of the time folder to analyze
length: float
Characteristic length :math:`L`, in :math:`m`
n_cells : int | None
Number of cells in the domain.
If None, it will deduced from the field reading
volume_time : str | None
Time folder to read to get the cell volumes.
If None, finds volume time automatically
field_dict : dict
Dictionary of fields used to avoid rereading the same fields to calculate different quantities

Returns
----------
froude_number: float
Froude number (dimensionless)
field_dict : dict
Dictionary of fields read
"""
if field_dict is None:
field_dict = {}
velocity, field_dict = compute_ave_liquid_velocity(
case_folder, time_folder, n_cells, volume_time, field_dict
)
gravity = read_gravity(case_folder)
return froude(velocity, length, gravity=gravity), field_dict


def compute_weber_number(
case_folder: str,
time_folder: str,
length: float,
n_cells: int | None = None,
volume_time: str | None = None,
field_dict: dict | None = None,
) -> tuple[float, dict]:
r"""Weber number from the liquid-averaged velocity/density and a given length.

:math:`We = \rho U^2 L / \sigma` with :math:`U` and :math:`\rho` the
liquid-volume-averaged velocity magnitude and density at ``time_folder``,
:math:`\sigma` from ``constant/phaseProperties``, and :math:`L` the passed
length.

Parameters
----------
case_folder: str
Path to case folder
time_folder: str
Name of the time folder to analyze
length: float
Characteristic length :math:`L`, in :math:`m`
n_cells : int | None
Number of cells in the domain.
If None, it will deduced from the field reading
volume_time : str | None
Time folder to read to get the cell volumes.
If None, finds volume time automatically
field_dict : dict
Dictionary of fields used to avoid rereading the same fields to calculate different quantities

Returns
----------
weber_number: float
Weber number (dimensionless)
field_dict : dict
Dictionary of fields read
"""
if field_dict is None:
field_dict = {}
velocity, field_dict = compute_ave_liquid_velocity(
case_folder, time_folder, n_cells, volume_time, field_dict
)
density, field_dict = compute_ave_liquid_density(
case_folder, time_folder, n_cells, volume_time, field_dict
)
surface_tension = read_surface_tension(case_folder)
return weber(density, velocity, length, surface_tension), field_dict


def compute_sherwood_number(
case_folder: str,
time_folder: str,
length: float,
species_name: str,
n_cells: int | None = None,
volume_time: str | None = None,
field_dict: dict | None = None,
) -> tuple[float, dict]:
r"""Sherwood number for one species from the instantaneous kL.

:math:`Sh = k_L L / D` with :math:`k_L` from
:func:`compute_instantaneous_kl` at ``time_folder`` and the molecular
diffusivity :math:`D` read as ``D_<species>`` from globalVars (the standard
Sherwood definition; the turbulent contribution is deliberately excluded).

Parameters
----------
case_folder: str
Path to case folder
time_folder: str
Name of the time folder to analyze
length: float
Characteristic length :math:`L`, in :math:`m`
species_name: str
Species for which to compute kL and use D_<species>
n_cells : int | None
Number of cells in the domain.
If None, it will deduced from the field reading
volume_time : str | None
Time folder to read to get the cell volumes.
If None, finds volume time automatically
field_dict : dict
Dictionary of fields used to avoid rereading the same fields to calculate different quantities

Returns
----------
sherwood_number: float
Sherwood number (dimensionless)
field_dict : dict
Dictionary of fields read
"""
if field_dict is None:
field_dict = {}
kl_spec, _, field_dict = compute_instantaneous_kl(
case_folder,
time_folder,
species_name,
n_cells=n_cells,
volume_time=volume_time,
field_dict=field_dict,
)
diffusivity = float(
read_global_vars(case_folder=case_folder, cross_ref=True)[
f"D_{species_name}"
]
)
# kL from compute_instantaneous_kl is in m/h, but D is in m2/s, so kL must
# be converted back to m/s for Sh to come out dimensionless.
kl_m_per_s = kl_spec[species_name] / 3600
return sherwood(kl_m_per_s, length, diffusivity), field_dict
Loading
Loading