Introduce FrictionFactorModel protocol with built‑in implementations and user extensibility - #816
Introduce FrictionFactorModel protocol with built‑in implementations and user extensibility#816dad616610 wants to merge 47 commits into
FrictionFactorModel protocol with built‑in implementations and user extensibility#816Conversation
…k, SwameeJain and Nikuradse
|
Ah, I forgot to mention #754 as the main reason behind this PR. Now a composite friction factor model can be created without modifying the pandapipes internals and now we have a built-in I've also improved the performance of Colebrook (up to ~1.76x in these benchmarks, see: 859f7f4). Benchmarking results
Code to reproduce
from functools import partial
from timeit import repeat as r
import numpy as np
max_iter = 10_000
tolerance = 1e-4
def colebrook(k_over_D, re):
lambda_prev = 1 / (-2 * np.log10(k_over_D / 3.71)) ** 2
lambda_curr = None
a = k_over_D / 3.71
b = 2.51 / re
# 1 / ln(10)
inv_ln10 = 0.4342944819032518276511289189166050822944
for _ in range(max_iter):
inv_lambda_sqrt = 1 / np.sqrt(lambda_prev)
inner_log_term = a + b * inv_lambda_sqrt
cubed_inv_lambda_sqrt = inv_lambda_sqrt**3
f = inv_lambda_sqrt + 2 * np.log10(inner_log_term)
df = (
-0.5 * cubed_inv_lambda_sqrt
- b * cubed_inv_lambda_sqrt * inv_ln10 / inner_log_term
)
lambda_curr = lambda_prev - f / df
if np.all(np.abs(lambda_curr - lambda_prev) < tolerance):
break
lambda_prev = lambda_curr
return lambda_curr
def colebrook_fast(k_over_D, re):
lambda_prev = 1 / (-2 * np.log10(k_over_D / 3.71)) ** 2
lambda_curr = None
a = k_over_D / 3.71
b = 2.51 / re
# 1 / ln(10)
inv_ln10 = 0.4342944819032518276511289189166050822944
for _ in range(max_iter):
inv_lambda_sqrt = 1 / np.sqrt(lambda_prev)
inner_log_term = a + b * inv_lambda_sqrt
cubed_inv_lambda_sqrt = inv_lambda_sqrt * inv_lambda_sqrt * inv_lambda_sqrt
f = inv_lambda_sqrt + 2 * np.log10(inner_log_term)
df = (
-0.5 * cubed_inv_lambda_sqrt
- b * cubed_inv_lambda_sqrt * inv_ln10 / inner_log_term
)
lambda_curr = lambda_prev - f / df
if np.all(np.abs(lambda_curr - lambda_prev) < tolerance):
break
lambda_prev = lambda_curr
return lambda_curr
def bench(sizes):
fs = (colebrook, colebrook_fast)
rng = np.random.default_rng(seed=42)
for n in sizes:
k = rng.uniform(1e-5, 1e-2, size=n)
d = rng.uniform(20, 800, size=n)
k_over_D = k / d
re = rng.uniform(2500, 8000, size=n)
print(f"{n = :_}")
for i, f in enumerate(fs):
res = r(
partial(f, k_over_D, re),
repeat=7,
number=100,
globals={**globals(), **locals()},
)
if i == 0:
baseline = min(res)
print(f"\t{f.__name__}: {min(res):.4f}")
else:
print(
f"\t{f.__name__}: {min(res):.4f}, speedup: {baseline / min(res):.4f}"
)
bench(sizes=np.array([10, 100, 1_000, 10_000, 100_000, 300_000])) |
Summary
This PR replaces the hard‑coded friction factor calculations with a
FrictionFactorModelprotocol, making it easy to use built‑in models (Nikuradse, Swamee‑Jain, Colebrook) or supply custom ones. ARegimeAwareFrictionFactorModelcan switch between models based on the Reynolds number. The old string‑based interface is still supported.Note for maintainers: Please squash this PR when merging.
Key changes
New
FrictionFactorModelprotocol – any callable implementingcompute_lambda_and_dlambda_dm()works with the pipe flow solver.Unified computation –$\lambda$ and $\frac{\mathrm{d}\lambda}{\mathrm{d}\dot{m}}$ , reusing intermediate results and keeping the interface simple.
calc_lambdaandcalc_der_lambdaare merged into a single method that returns bothBuilt‑in models – Nikuradse, SwameeJain, Colebrook, and RegimeAwareFrictionFactorModel.
Unified Nikuradse formulations – the previously separate compressible and incompressible Nikuradse functions were mathematically equivalent1; now only one implementation remains. Though I had to relax the tolerance in the tests (see commit 9960d71).
Consistent pre‑filtering – zero‑flow and zero‑length pipes are now filtered once before any model is called, instead of being handled inside each model.
Removed Numba support from friction factor models (rationale2: easier custom numba‑models by users, and no clean way to integrate
use_numbaautomatically). This can be revisited later.Documentation – new pipeflow/friction_factor_models page; the pipe component page now refers to it.
Backward compatibility – string names like "nikuradse" are converted to the appropriate model instance internally.
Known limitations / follow‑ups
The Nikuradse derivative currently uses$|m|$ instead of the physically correct odd form $-\frac{64}{Re \cdot m}$ . Fixing it slightly degrades convergence; this will be addressed separately.
The Colebrook error message still mentions
max_iter_colebrook; a follow‑up can adjust it for the new API.Numba support for friction models can be re‑added if performance demands it; custom numba‑based models can be used now.
Impact
Enables users to implement and use any friction factor model without modifying pandapipes internals.
Potentially closes add friction model "Hofer" #683 and Implemented new friction factor calculation spanning entire flow regime #767 as these friction factor models can now be implemented from user-side.
Footnotes
Proof of mathematical equivalence
On numba support removal
Numba starts beating pure-numpy functions with >10k elements -- below they're almost equal. I ran the benchmark tests for different sizes of elements. Each function was run 100 times per round, with 7 rounds. The best (minimum) time across the 7 rounds is shown in seconds.
Nikuradse results
Swamee-Jain results
Colebrook results
Benchmarking code