A Streamlit web app and Python library for predicting how porosity defects degrade the strength and stiffness of fiber-reinforced composite laminates.
Manufacturing defects like porosity are inevitable in composite structures. Engineers need to quantify how much strength is lost for a given porosity level, void morphology, and loading mode. PorosityFE provides:
- Fast empirical screening using calibrated Judd-Wright and power-law models
- 3D finite element analysis with Eshelby-based stiffness degradation at each element
- Layup-aware predictions that account for how ply orientation affects porosity sensitivity
- Interactive web app for rapid parametric studies without writing code
- Two porosity types: Distributed microporosity (continuous field) + discrete macrovoids (explicit geometry)
- Three distribution models: Uniform, clustered (midplane/surface/quarter), interface-concentrated
- Three void morphologies: Spherical, cylindrical (prolate), penny-shaped (oblate)
- Four loading modes: Compression, tension, shear, ILSS
- Three empirical knockdown models: Judd-Wright (exponential), power-law, linear
- Two solver tiers: Empirical correlations (fast) + 3D finite element (detailed)
- Six material presets: T800/epoxy, T700/epoxy, E-glass/epoxy, IM7/8551, T300/934, CF/PEEK (or define your own)
- Discrete void modeling: Explicit ellipsoidal voids with stress concentration factors
- Tsai-Wu failure criterion: Full 3D multiaxial strength evaluation
git clone https://github.com/elhajjar1/PorosityFE.git
cd PorosityFE
pip install -e ".[all]"pip install -r requirements.txt # core (CLI + library)
pip install -r requirements-web.txt # add Streamlit for the web apprequirements.txt covers the CLI and Python library only. Streamlit is an
optional web extra (see pyproject.toml); install it via
requirements-web.txt or pip install -e ".[web]" if you want to run
streamlit run app.py.
pytest tests/ -vstreamlit run app.pyOpens a browser UI at http://localhost:8501 with sidebar inputs (material, layup,
porosity, loading mode, mesh) and tabs for the porosity profile, mesh,
knockdown bar chart, FE stress contours, and result downloads. See
DEPLOYMENT_STREAMLIT.md for hosting on Streamlit
Community Cloud.
porosity-analyzeRuns the full analysis across 5 porosity levels (1%-8%) and 5 configurations, generating PNG plots and JSON results.
from porosity_fe import *
# Quick empirical screening
material = MATERIALS['T800_epoxy']
pf = PorosityField(material, 0.05, distribution='interface', void_shape='penny')
mesh = CompositeMesh(pf, material, nx=50, ny=20, nz=24)
solver = EmpiricalSolver(mesh, material)
result = solver.get_failure_load(mode='compression', model='judd_wright')
print(f"Knockdown: {result.knockdown:.3f}")
# Compare all configurations at 3% porosity
results = compare_configurations(0.03, material_name='T800_epoxy')Six material presets are available in the MATERIALS dict (see the
Features list). To analyze a system that isn't in the built-in
set, construct a MaterialProperties instance directly and pass it through
the analysis pipeline — there is no separate registration step required.
from porosity_fe import MaterialProperties, PorosityField, CompositeMesh, EmpiricalSolver
# Define a custom T700 / toughened-epoxy system
my_material = MaterialProperties(
# Stiffness (MPa)
E11=140000.0, E22=10500.0, E33=10500.0,
G12=4900.0, G13=4900.0, G23=3700.0,
nu12=0.30, nu13=0.30, nu23=0.42,
# Strength (MPa)
sigma_1c=1300.0, sigma_1t=2500.0,
sigma_2t=70.0, sigma_2c=210.0,
tau_12=90.0, tau_ilss=85.0,
# Geometry
t_ply=0.180, n_plies=24,
# Constituents (used by the FE solver's micromechanics path)
matrix_modulus=3400.0, matrix_poisson=0.36,
fiber_modulus=240000.0, fiber_volume_fraction=0.60,
)
pf = PorosityField(my_material, void_volume_fraction=0.03, distribution='uniform')
mesh = CompositeMesh(pf, my_material, nx=30, ny=10, nz=12)
solver = EmpiricalSolver(mesh, my_material)
print(solver.get_failure_load(mode='compression', model='judd_wright'))Notes:
- All stiffnesses and strengths are MPa; thicknesses are mm.
- All
MaterialPropertiesfields are required — there are no defaults for the engineering constants. Cross-check anisotropy bounds (ν12 < 0.5,E11 > E22, etc.) before running. - The FE micromechanics path uses
matrix_modulus,matrix_poisson,fiber_modulus, andfiber_volume_fractionto compute the Eshelby stiffness degradation. Supply realistic constituent values even if you only plan to run the empirical solver — both paths share the sameMaterialPropertiesobject. - To use this material with the included CLI / batch scripts, you can also
add it to the
MATERIALSdict inporosity_fe/materials.pyso it can be selected by name (compare_configurations(..., material_name='my_system')). - The empirical knockdown coefficients (
alpha,n,beta) live onEmpiricalSolver, not onMaterialProperties. To recalibrate them for a non-standard material system, see Calibratingalpha/nfor a custom material.
Runnable scripts under examples/ cover the most common
configurations end-to-end. Each script is standalone (~30–50 lines), prints
a short result table, and saves a single PNG to examples/output/ (which
is gitignored). Run any of them from the repo root:
python examples/uniform_spherical.py| Script | What it demonstrates |
|---|---|
examples/uniform_spherical.py |
Uniform porosity, spherical voids; empirical compression knockdown (Judd-Wright). |
examples/clustered_midplane.py |
Gaussian-clustered porosity at the midplane; ILSS knockdown. |
examples/interface_penny.py |
Interface-concentrated porosity with penny-shaped voids — the worst-case ILSS morphology. |
examples/discrete_voids.py |
Explicit VoidGeometry ellipsoids on top of a low-uniform background; renders the SCF field around one void. |
examples/compute_degraded_clt_moduli.py |
CLT path: laminate effective moduli (Ex, Ey, Gxy) vs. Vp for a quasi-isotropic layup. |
examples/distribution_comparison.py |
Side-by-side uniform vs clustered vs interface at matched Vp_mean; shows empirical KDs collapse, FE KDs diverge (#83). |
In addition to the mode / model kwargs shown above, EmpiricalSolver.get_failure_load
also accepts an environment= kwarg (hygrothermal knockdown — temperature
and moisture; PR #59) and a cycles= kwarg (fatigue knockdown). Both
compose multiplicatively with the porosity knockdown; see the
EmpiricalSolver.get_failure_load docstring for the accepted shapes and
defaults.
See examples/README.md for the full index and the
conventions reminder. Math conventions (Voigt order, engineering vs. tensor
strain, compression-sign) are documented at the API surface — look for the
Notes block on MaterialProperties.get_stiffness_matrix,
Hex8Element.B_matrix, FieldResults, and EmpiricalSolver.apply_loading.
pip install pyinstaller
python -m PyInstaller ValidatePorosity.spec --noconfirm --clean
# Linux/macOS: dist/validate_porosity/validate_porosity
# Windows: dist\validate_porosity\validate_porosity.exePre-built executables for all three platforms are produced automatically
by GitHub Actions on every push; download them from the Actions tab
(artifact names: validate_porosity-linux, -macos, -windows) or
from the Releases page for tagged versions.
CLI usage:
validate_porosity --help # show all options
validate_porosity # run against bundled datasets, write to cwd
validate_porosity --output-dir /tmp # write reports elsewhere
validate_porosity --quiet # suppress progress outputPorosityField supports four through-thickness shapes, all renormalized
so the specimen-average void volume fraction equals the input Vp:
distribution (+ cluster_location) |
Through-thickness shape |
|---|---|
uniform |
Constant Vp at every z. |
clustered + cluster_location='midplane' |
Gaussian peak at the midplane (sigma = Lz / 6). |
clustered + cluster_location='surface' |
Gaussian peak at the laminate surface. |
interface |
Sum of Gaussians centred on every ply-to-ply interface (sigma = 0.35 * t_ply). |
Note on terminology. There is no preset literally named stack; the
"stacked / layered" non-uniform shapes are clustered (a single Gaussian
bump) and interface (a comb of bumps at every ply interface). The
issue tracker historically used "stack" informally to refer to either.
The empirical correlations (judd_wright, power_law, linear) were
calibrated against specimen-average porosity, so
EmpiricalSolver.get_failure_load evaluates them at the mean Vp
(self.mesh.porosity_field.Vp). At matched Vp_mean, all four
distributions therefore produce identical empirical knockdowns:
mode = compression, model = judd_wright, Vp_mean = 3 %
uniform 0.813020
clustered (midplane) 0.813020
clustered (surface) 0.813020
interface 0.813020
The FE solver, by contrast, samples Vp(x, y, z) at every node and
degrades the local stiffness pointwise, so it does pick up the
peak-vs-mean difference. Running the same four cases through FESolver
gives distinct compression knockdowns even at matched mean:
mode = compression, FE Tsai-Wu, Vp_mean = 3 %
uniform 0.9887
clustered (midplane) 0.9882
clustered (surface) 0.9883
interface 0.9854 (penny voids, sharpest local field)
(Numbers reproduced by python examples/distribution_comparison.py;
exact values depend on mesh resolution — the relative ordering is
robust.)
- First-pass screening / NCR validation summary — use
uniform. When only a single specimen-averageVpfrom a C-scan / ultrasonic measurement is available, the distribution choice does not change the empirical answer.uniformis the default in the NCR validation summary for exactly this reason. - X-ray CT shows a through-thickness gradient — use
clustered (midplane)(orinterfaceif the voids cluster at ply drops). Then run the FE path so the field solver can resolve the local peak. The empirical row will still be the same asuniform; the FE row is where the difference shows up.
A runnable side-by-side comparison (table + two PNGs) lives in
examples/distribution_comparison.py.
Both solver paths predict porosity knockdown, but they use different
physics for the strength-degradation step. The empirical solver
(EmpiricalSolver) applies calibrated closed-form correlations
(Judd-Wright, power-law, linear) fit to coupon data. The FE solver
(FESolver) instead applies a heuristic sqrt scaling on per-component
ply strengths (strength ~ sqrt(stiffness_retention); see
FESolver._degraded_strengths). These are fundamentally different
mathematical forms, so the two paths will give numerically different
knockdowns for the same (layup, Vp) -- this divergence is by design,
not a bug.
Guidance on which to use:
- Empirical solver -- fast screening, headline knockdown numbers, and comparison against published coupon data.
- FE solver -- stress-field analysis, per-element failure criteria (Tsai-Wu / Hashin / max-stress), and any study that needs the full mesh-level result.
| File Pattern | Description |
|---|---|
porosity_profile_*.png |
Through-thickness porosity profiles |
porosity_mesh_3d_*.png |
3D mesh with hexahedral elements |
porosity_mesh_detail_*.png |
Cross-section element detail |
porosity_damage_*.png |
Stiffness reduction contour maps |
porosity_comparison_*.png |
Model comparison bar charts |
porosity_knockdown_curves.png |
Knockdown vs porosity curves |
porosity_analysis_results_*.json |
Numerical results (JSON) |
The web app's Export tab provides one-click downloads of the active run's empirical knockdown table as either JSON or CSV. CSV files include the analysis configuration as #-prefixed comment lines at the top (which pandas, Excel, and MATLAB all ignore by default), followed by a flat mode,model,failure_stress_MPa,knockdown table.
Vp is always a dimensionless fraction in [0, 1] — never a percent.
| You want to model | Pass | Do not pass |
|---|---|---|
| 1% porosity | Vp = 0.01 |
Vp = 1.0 |
| 3% porosity | Vp = 0.03 |
Vp = 3.0 |
| 10% porosity | Vp = 0.10 |
Vp = 10.0 |
The plotting axes display Vp * 100 (%) for readability; that is a display
convention only. The constructor (PorosityField(..., void_volume_fraction=Vp))
rejects values outside [0, 1] with a ValueError and offers a percent-vs-fraction
hint when the value is plausibly a percent (Vp ≥ 1.001).
The empirical knockdown path (EmpiricalSolver) treats Vp as the
specimen-average void volume fraction. Strength is degraded once at the
laminate level via σ(Vp) = KD(Vp) · σ₀; modulus reduction is not applied
per-ply by the empirical solver.
The FE path (FESolver) builds a 3D hexahedral mesh and applies stiffness
degradation per element, with the local porosity at each element coming
from PorosityField.local_porosity(x, y, z). The through-thickness profile
depends on the distribution argument:
distribution |
Meaning |
|---|---|
'uniform' |
Same Vp in every element (and every ply). |
'clustered' |
Gaussian profile centered at cluster_location (midplane / surface / quarter), normalized so the through-thickness mean equals Vp. |
'interface' |
Gaussian peaks at every ply-to-ply interface, normalized so the through-thickness mean equals Vp. |
In all three cases the input Vp is the target specimen average; the
profile is normalized so that averaging it over the full thickness recovers
Vp (subject to discrete-void contributions, which are added afterwards).
Layup orientation enters the empirical path through the matrix-dominated
fraction f_md (see Layup scaling below) — the model
penalizes matrix-dominated layups more than fiber-dominated ones for the
same Vp.
Both empirical models below take the specimen-average void volume fraction Vp as a dimensionless fraction in [0, 1] (e.g. 3% porosity → Vp = 0.03, never Vp = 3.0). Each returns a knockdown factor KD ∈ (0, 1] that multiplies the pristine strength σ₀.
Judd-Wright (exponential decay):
KD = exp(-alpha * Vp)
alphais an empirical sensitivity coefficient. It is dimensionless whenVpis a fraction.- For small
Vp,KD ≈ 1 − alpha · Vp, soalphais approximately the fractional strength loss per unitVp. Judd & Wright (1978) reported ILSS dropping ~7% per 1% voids in CFRP, which corresponds toalpha ≈ 7. - The exponential form is an engineering convention re-fit of Judd & Wright's linear data; it is well-behaved up to
Vp ≈ 0.04–0.05and over-penalizes higher porosities.
Power Law:
KD = (1 - Vp)^n
nis a phenomenological exponent rooted in Mackenzie (1950) spherical-void elasticity and generalized empirically (Rice, 2005).n = 1corresponds to a simple area-reduction rule of mixtures;n > 1captures stress concentration around voids.
Linear:
KD = max(1 - beta * Vp, 0)
betais the same kind of sensitivity coefficient asalphain Judd-Wright (dimensionless whenVpis a fraction;beta ≈ -ΔKD / ΔVp). The linear form matches the original Judd & Wright (1978) "ILSS drops ~7% per 1% voids" reading directly, and is included for screening and easy hand-checks. It saturates to zero atVp = 1/betaand is best used at lowVp(typically< 0.05).
f_md_ref = 0.5 is the layup-scaling reference (scale = 1.0 at f_md = 0.5); the QI coefficients below were tuned with this scaling already applied, so they are NOT raw fits to a single layup but represent the model's effective-baseline values:
| Loading mode | alpha (Judd-Wright) |
n (Power-Law) |
beta (Linear) |
|---|---|---|---|
| Compression | 6.9 | 2.8 | 5.5 |
| Tension | 3.9 | 1.8 | 3.5 |
| Shear (in-plane) | 8.0 | 3.5 | 7.0 |
| ILSS | 10.0 | 4.5 | 9.0 |
These values sit inside published CFRP ranges: alpha ≈ 1–3 for fiber-dominated tension and 5–10 for matrix-dominated ILSS / flexure; n ≈ 1–2 for stiffness-like properties and 3–5 for compression / ILSS.
PorosityFE adapts the QI coefficients to the user's layup via a matrix-dominated fraction f_md computed from the ply angles (0° → 0.0, ±45° → 0.5, 90° → 1.0):
alpha_eff(mode) = alpha_QI(mode) * (f_md / 0.5)
n_eff(mode) = max(n_QI(mode) * (f_md / 0.5), 0.1)
A floor of 0.15 is applied to the scale (0.80 for ILSS, which is always matrix-dominated). The _F_MD_FLOOR / _F_MD_FLOOR_ILSS constants in EmpiricalSolver are empirical floors documented inline in the source (see issue #139 for the calibration gap):
| Layup | f_md |
scale | alpha_eff (compression) |
|---|---|---|---|
[0]_16 (UD) |
0.00 | 0.15 (floor) | 1.035 |
[±45]_4s (off-axis) |
0.50 | 1.00 | 6.90 |
[90]_8 (transverse) |
1.00 | 2.00 | 13.80 |
Matrix-dominated layups are penalized more by porosity than fiber-dominated layups, as expected physically.
The calibration data covers Vp ≲ 0.05. Beyond that, both forms should be treated as extrapolations. Vp alone does not capture void morphology (size, aspect ratio, clustering), which is a known source of scatter — use the FE solver path when spatial stress-concentration detail is needed.
If your material system differs significantly from the calibration set:
- Manufacture a ladder of coupons spanning
Vp ≈ 0–5%(vary autoclave debulk pressure or cure vacuum). - Measure void content per ASTM D2734 (matrix burnoff) or D3171 (acid digestion); cross-check by μCT or polished cross-section.
- Run the relevant strength test: ASTM D2344 (ILSS / short-beam shear), D7264 (flexure), D3039 (tension), or D6641 (compression).
- Normalize each datum by the void-free baseline:
KD = σ(Vp) / σ(0). - Regress
ln(KD)vsVp(slope= −alpha) for Judd-Wright, orln(KD)vsln(1 − Vp)(slope= n) for the power law.
Custom alpha / n / beta values are exposed as keyword-only constructor arguments on EmpiricalSolver. Overrides are partial (modes you don't pass keep the calibrated defaults) and are layup-scaled exactly like the built-in coefficients:
# Fitted ILSS alpha for a custom material; other modes keep QI defaults.
solver = EmpiricalSolver(
mesh, material,
judd_wright_alpha={'ilss': 12.0},
power_law_n={'ilss': 5.2},
linear_beta={'ilss': 11.0},
)At the QI reference layup (f_md = 0.5, scale = 1.0) the override is used directly; for a different layup it scales the same way as the QI baseline (e.g. with a UD layup, judd_wright_alpha={'ilss': 12.0} becomes an effective 12.0 × 0.80 = 9.6 because ILSS uses a 0.80 floor — see Layup scaling). Override values must be positive finite numbers; mode keys must be a subset of {'compression', 'tension', 'shear', 'ilss'}.
The FE solver builds a 3D hexahedral mesh and degrades element stiffness based on local porosity:
- Eshelby inclusion theory computes degraded matrix properties (voids as zero-stiffness ellipsoids)
- Micromechanics rules map matrix degradation to composite degradation ratios for E11, E22, G12
- Tsai-Wu criterion evaluates multiaxial failure at each integration point
Full 3D Tsai-Wu with degraded strengths:
F1*s1 + F2*s2 + F11*s1^2 + F22*s2^2 + F66*s6^2 + 2*F12*s1*s2 = 1
Tsai-Wu F_12 convention (caveat for external comparisons). When
benchmarking PorosityFE Tsai-Wu predictions against another code or
reference dataset, confirm the in-plane interaction coefficient F_12
convention matches. PorosityFE uses Tsai's recommendation
F_12 = -0.5 * sqrt(F_11 * F_22) (Tsai & Wu, 1971) by default — an
empirical choice, not a first-principles derivation. The exact value
varies with the material system; if you have biaxial coupon
calibration data, override it per-material via
MaterialProperties(tsai_wu_F12=...) (must lie in [-1, 0] for a
closed envelope).
PorosityFE is validated against 13 peer-reviewed experimental datasets
covering carbon/epoxy, IM7/toughened epoxy, T300/epoxy systems, and
CF/PEEK thermoplastic. Validation is automated via validate_porosity
CLI (pre-built for Linux/macOS/Windows on the Releases page)
or in-process via validation/validate_all.py.
Model scope (validated properties):
| Property | # papers | Overall MAE |
|---|---|---|
| ILSS (short-beam shear) | 9 | 4.3% |
| Tensile strength | 7 | 6.9% |
| Tensile modulus | 3 | 1.3% |
| Transverse tensile modulus | 3 | 3.4% |
| Transverse tensile strength | 3 | 7.4% |
| Flexural modulus (D-matrix CLT) | 5 | 8.9% |
| Compression strength | 2 | 11.4% |
| Shear strength | 2 | 13.5% |
| Shear modulus (A-matrix CLT) | 1 | 15.4% |
Overall MAE:
- Property-weighted: 7.09% across 35 (paper, property) pairs (each entry weighted equally — what
validate_porosityreports as the headline). - Point-weighted: 6.56% across 239 individual (Vp, normalized) data points (each measurement weighted equally — the standard convention in regression-error reporting).
The two aggregations differ because datasets carry very different numbers of points; validate_porosity prints both in the run summary.
- Empirical models are calibrated for porosity levels up to ~10%
- FE solver uses analytical stiffness degradation (not full nonlinear FE)
- Thermal residual stresses are not included
- Fatigue and environmental effects are not modeled
- Delamination initiation/propagation is not explicitly simulated
- Flexural strength was removed from the validation database in v1.1.1 because 3-point bend failure involves mixed compression + interlaminar shear mechanisms that the Judd-Wright mode proxy cannot capture reliably (observed MAE 8-40% across papers)
If you use PorosityFE in your research, please cite:
@software{elhajjar2026porosityfe,
author = {Elhajjar, Rani},
title = {{PorosityFE}: Porosity-Degraded Composite Laminate Analysis},
year = {2026},
url = {https://github.com/elhajjar1/PorosityFE},
version = {1.2.0}
}Related publication:
Elhajjar, R. (2025). Fat-tailed failure strength distributions and manufacturing defects in advanced composites. Scientific Reports, 15, 25977. DOI: 10.1038/s41598-025-06693-4
- Judd & Wright (1978) - Voids and their effects on the mechanical properties of composites — an appraisal. SAMPE Journal 14(1), 10–14
- Mackenzie (1950) - The elastic constants of a solid containing spherical holes. Proc. Phys. Soc. B 63(1), 2–11
- Rice (2005) - Use of normalized porosity in models for the porosity dependence of mechanical properties. J. Mater. Sci. 40, 983–989
- Eshelby (1957) - The determination of the elastic field of an ellipsoidal inclusion
- Mura (1987) - Micromechanics of Defects in Solids
- Tsai & Wu (1971) - A general theory of strength for anisotropic materials
- Elhajjar (2025) - Fat-tailed failure strength distributions and manufacturing defects
See CONTRIBUTING.md for guidelines on reporting bugs, suggesting features, and submitting pull requests.
MIT License. See LICENSE for details.


