Module property
=============================================================================== SFPPy Module: Property =============================================================================== Defines physical parameters for mass transfer, independent of specific theoretical or empirical models. Currently implements the Piringer model for worst-case migration simulations.
Main Components:
- Base Class: migrationProperty
(Holds generic attributes for any mass transfer property)
- Subclasses for Specific Properties:
- Diffusivities
: Defines diffusion coefficients (D)
- HenriLikeCoeffcicients
: Defines Henry-like coefficients (k)
- ActivityCoeffcicients
: Defines activity coefficients (γ)
- PartitionCoeffcicients
: Defines partition coefficients (K)
- Piringer Model (Dpiringer
)
- Empirical overestimation model for polymer diffusion
- Used for migration simulations in migration.py
- Directly invoked by loadpubchem.py
when retrieving substance properties
Integration with SFPPy Modules:
- Used by loadpubchem.py
to predict missing diffusivity or partitioning values for chemical migrants.
- Applied in migration.py
for solving mass transfer equations.
Example:
from property patankar.import Dpiringer
D_value = Dpiringer.evaluate(polymer="LDPE", M=100, T=40)
=============================================================================== Details =============================================================================== This module offers the necessary abstraction to any physical parameter governing mass transfer independently of the applied molecular theory or emprical model used to calculate them.
Currently this module implements seamlesly the Dpringer model for worst-case simulations for risk assessment.
This class is used directly by loadpubchem without involving any user operation. The name or CAS of the substance will trigger the predictions for the considered application.
@version: 1.21 @project: SFPPy - SafeFoodPackaging Portal in Python initiative @author: INRAE\olivier.vitrac@agroparistech.fr @licence: MIT @Date: 2024-07-21 @rev: 2025-03-03
Expand source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
===============================================================================
SFPPy Module: Property
===============================================================================
Defines physical parameters for mass transfer, independent of specific theoretical or empirical models.
Currently implements the **Piringer model** for worst-case migration simulations.
**Main Components:**
- **Base Class: `migrationProperty`** (Holds generic attributes for any mass transfer property)
- **Subclasses for Specific Properties:**
- `Diffusivities`: Defines diffusion coefficients (D)
- `HenriLikeCoeffcicients`: Defines Henry-like coefficients (k)
- `ActivityCoeffcicients`: Defines activity coefficients (γ)
- `PartitionCoeffcicients`: Defines partition coefficients (K)
- **Piringer Model (`Dpiringer`)**
- Empirical overestimation model for polymer diffusion
- Used for migration simulations in `migration.py`
- Directly invoked by `loadpubchem.py` when retrieving substance properties
**Integration with SFPPy Modules:**
- Used by `loadpubchem.py` to predict missing diffusivity or partitioning values for chemical migrants.
- Applied in `migration.py` for solving mass transfer equations.
Example:
```python
from property patankar.import Dpiringer
D_value = Dpiringer.evaluate(polymer="LDPE", M=100, T=40)
```
===============================================================================
Details
===============================================================================
This module offers the necessary abstraction to any physical parameter governing
mass transfer independently of the applied molecular theory or emprical model used
to calculate them.
Currently this module implements seamlesly the Dpringer model for worst-case simulations
for risk assessment.
This class is used directly by loadpubchem without involving any user operation.
The name or CAS of the substance will trigger the predictions for the considered application.
@version: 1.21
@project: SFPPy - SafeFoodPackaging Portal in Python initiative
@author: INRAE\\olivier.vitrac@agroparistech.fr
@licence: MIT
@Date: 2024-07-21
@rev: 2025-03-03
"""
import numpy as np
__all__ = ['ActivityCoefficients', 'Diffusivities', 'Dpiringer', 'HenryLikeCoefficients', 'MigrationPropertyModel_validator', 'PartitionCoeffcicients', 'gFHP', 'kFHP', 'migrationProperty']
__project__ = "SFPPy"
__author__ = "Olivier Vitrac"
__copyright__ = "Copyright 2022"
__credits__ = ["Olivier Vitrac"]
__license__ = "MIT"
__maintainer__ = "Olivier Vitrac"
__email__ = "olivier.vitrac@agroparistech.fr"
__version__ = "1.21"
# %% Top classes for any property
# level 0
class migrationProperty:
"""Base class to hold general properties used for migration of substances."""
property = "any"
notation = ""
description = "root class"
name = "root"
parameters = [] # e.g. ["M", "T"]
SIunits = ""
# private properties
_model = ""
_theory = ""
_source = ""
_author = "olivier.vitrac@agroparistech.fr"
_license = "MIT"
_version = 1.21
_available_to_import = False
def __repr__(self):
"""Formatted string representation for nice display."""
# Define attribute names and their corresponding values
attributes = {
"property": self.property,
"notation": self.notation,
"description": self.description,
"name": self.name,
"parameters": self.parameters,
"SIunits": self.SIunits,
"model": self._model,
"theory": self._theory,
"source": self._source,
"author": self._author,
"license": self._license,
"version": self._version,
}
# Filter out None or empty string values
filtered_attributes = {k: v for k, v in attributes.items() if v not in (None, "")}
# Find the max length of attribute names for alignment
max_key_length = max(len(k) for k in filtered_attributes.keys()) if filtered_attributes else 0
# Format the output with proper alignment
lines = [f"{k.rjust(max_key_length)}: {v}" for k, v in filtered_attributes.items()]
print("\n".join(lines))
return str(self)
def __str__(self):
"""Formatted string representation of property"""
return f"<{self.__class__.__name__}: {self.property}:{self.notation}>"
# level 1
class Diffusivities(migrationProperty):
"""Base class for diffusion-related models."""
property = "Diffusivity"
notation = "D"
description = "Mathematical model to estimate diffusivities"
SIunits = "m**2/s"
class HenryLikeCoefficients(migrationProperty):
property = "Henri-like coefficient"
notation = "k"
description = "Mathematical model to estimate Henri-like coefficients"
SIunits = None
class ActivityCoefficients(migrationProperty):
property = "Activity coefficient"
notation = "g"
description = "Mathematical model to estimate activity coefficients"
SIunits = None
class PartitionCoeffcicients(migrationProperty):
property = "Partition Coefficient"
notation = "K"
description = "Mathematical model to estimate partition coefficients"
SIunits = None
# %% Simplified Flory-Huggins model of Henry-like coefficients built on gFHP
class kFHP(HenryLikeCoefficients):
"""
Simplified model to estimate Henry-like coefficients based on gFHP class
ki,k = Vi * Pi gik(P'i,P'k,Vi,Vk)
i: solute
k: P or F
P'i and P'k: Polarity index (e.g.: migrant("solute").polarityindex)
Vi, Vk: molar volumes (e.g. migrant("solute").molarvolumeMiller)
ispolymer: True for polymers
alpha: scaling constant for chiik (default=0.14=1/migrant("water").polarityindex)
lngmin: minimum value (default=0)
gscale: activity coefficient (default=Vi*Psat)
Psat: vapor saturation pressure
Only a static evaluate is proposed.
Note use: scaling = False to get activity coefficients instead of Henry-like ones
"""
name = "kFHP"
description = "Flory-Huggins model of Henry-likecoefficients from P' and V at infinite dilution in k"
model = "semi-empirical"
theory = "Flory-Huggins"
parameters = {"Pi": {"description": "polarity index of solute i","units": "-"},
"Pk": {"description": "polarity index of continuous phase k","units": "-"},
"Vi": {"description": "molar volume","units": "g/cm**3"},
"Vk": {"description": "molar volume of k","units": "g/cm**3"},
"Psat": {"description": "vapor saturation pressure of i","units": "Pa"}
}
_available_to_import = True # this model can be imported
@classmethod
def evaluate(cls, Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer = False, alpha=0.14,lngmin=0.0,Psat=1.0,scaling=True):
"""evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)"""
if scaling: # (default behavior)
return gFHP.evaluate(Pi=Pi, Pk=Pk, Vi=Vi, Vk=Vk, ispolymer=ispolymer,
alpha=alpha,lngmin=lngmin,
gscale=Vi*1e-3*Psat # Vi is converted [cm**3/g] --> [m**3/kg]
)
else:
return gFHP.evaluate(Pi=Pi, Pk=Pk, Vi=Vi, Vk=Vk, ispolymer=ispolymer,
alpha=alpha,lngmin=lngmin,
gscale=1
)
# %% Simplified Flory-Huggins model of activity coefficients
class gFHP(ActivityCoefficients):
"""
Simplified model to estimate activity coefficients gik from P'i, P'k, Vi, Vk
i: solute
k: P or F
P'i and P'k: Polarity index (e.g.: migrant("solute").polarityindex)
Vi, Vk: molar volumes (e.g. migrant("solute").molarvolumeMiller)
ispolymer: True for polymers
alpha: scaling constant for chiik (default=0.14=1/migrant("water").polarityindex)
lngmin: minimum value (default=0)
gscale: activity coefficient (default=1.0)
Use gscale to enforce gik<=1
Only a static evaluate is proposed.
"""
name = "gFHP"
description = "Flory-Huggins model of activity coefficients from P' and V at infinite dilution in k"
model = "semi-empirical"
theory = "Flory-Huggins"
parameters = {"Pi": {"description": "polarity index of solute i","units": "-"},
"Pk": {"description": "polarity index of continuous phase k","units": "-"},
"Vi": {"description": "molar volume of i","units": "-"},
"Vk": {"description": "molar volume of k","units": "-"}
}
_available_to_import = True # this model can be imported
@classmethod
def evaluate(cls, Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer = False,
alpha=0.14,lngmin=0.0,gscale=1.0):
"""evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)"""
if ispolymer:
rik = 0.0
nik = 0.0
chimin = 0.25
else:
rik = Vi/Vk
nik = (rik - 5)/5
chimin = 0
chiik = np.maximum(chimin,alpha * (Pi - Pk)**2)
lngik = np.maximum(lngmin,chiik + 1 - (rik - nik))
return gscale * np.exp(lngik)
# %% Piringer model
class Dpiringer(Diffusivities):
"""
Piringer's overestimate of diffusion coefficient.
Two implementations are offered in the class:
- static: Dpiringer.evaluate(polymer="polymer",M=Mvalue,T=Tvalue)
- dynamic: Dmodel = Dpiringer(polymer="polymer"...)
Dmodel.eval(M=Mvalue,T=Tvalue)
"""
name = "Piringer"
description = "Piringer's overestimate of diffusion coefficients"
model = "empirical"
theory = "scaling"
parameters = {"M": {"description": "molecular mass","units": "g/mol"},
"T": {"description": "temperature","units": "degC"}
}
_available_to_import = True # this model can be directly imported
# Piringer values (the primary key matches the one used in layer)
piringer_data = {
# -- polyolefins -------------------------------------------
"HDPE": { # category: polyolefins
"className": "HDPE",
"type": "polymer",
"material": "high-density polyethylene",
"code": "HDPE",
"description": "Piringer parameters for HDPE.",
"App": 14.5,
"tau": 1577
},
"LDPE": { # category: polyolefins
"className": "LDPE",
"type": "polymer",
"material": "low-density polyethylene",
"code": "LDPE",
"description": "Piringer parameters for LDPE.",
"App": 11.5,
"tau": 0
},
"LLDPE": { # category: polyolefins
"className": "LLDPE",
"type": "polymer",
"material": "linear low-density polyethylene",
"code": "LLDPE",
"description": "Piringer parameters for LLDPE.",
"App": 11.5,
"tau": 0
},
"PP": { # category: polyolefins
"className": "PP",
"type": "polymer",
"material": "isotactic polypropylene",
"code": "PP",
"description": "Piringer parameters for isotactic PP.",
"App": 13.1,
"tau": 1577
},
"aPP": { # category: polyolefins
"className": "PPrubber",
"type": "polymer",
"material": "atactic polypropylene",
"code": "aPP",
"description": "Piringer parameters for atactic PP.",
"App": 11.5,
"tau": 0
},
"oPP": { # category: polyolefins
"className": "oPP",
"type": "polymer",
"material": "bioriented polypropylene",
"code": "oPP",
"description": "Piringer parameters for bioriented PP.",
"App": 13.1,
"tau": 1577
},
# -- polyvinyls --------------------------------------------
"pPVC": { # category: polyvinyls
"className": "plasticizedPVC",
"type": "polymer",
"material": "plasticized PVC",
"code": "pPVC",
"description": "Piringer parameters for plasticized PVC.",
"App": 14.6,
"tau": 0
},
"PVC": { # category: polyvinyls
"className": "rigidPVC",
"type": "polymer",
"material": "rigid PVC",
"code": "PVC",
"description": "Piringer parameters for rigid PVC.",
"App": -1.0,
"tau": 0
},
# -- polystyrene, etc. (misc) ------------------------------
"HIPS": { # category: polystyrenics
"className": "HIPS",
"type": "polymer",
"material": "high-impact polystyrene",
"code": "HIPS",
"description": "Piringer parameters for HIPS.",
"App": 1.0,
"tau": 0
},
"PBS": { # category: polystyrenics
"className": "PBS",
"type": "polymer",
"material": "styrene-based polymer PBS",
"code": "PBS",
"description": "No original Piringer data; set to None.",
"App": 10.5,
"tau": 0
},
"PS": { # category: polystyrenics
"className": "PS",
"type": "polymer",
"material": "polystyrene",
"code": "PS",
"description": "Piringer parameters for PS.",
"App": -1.0,
"tau": 0
},
# -- polyesters --------------------------------------------
"PBT": { # category: polyesters
"className": "PBT",
"type": "polymer",
"material": "polybutylene terephthalate",
"code": "PBT",
"description": "Piringer parameters for PBT.",
"App": 6.5,
"tau": 1577
},
"PEN": { # category: polyesters
"className": "PEN",
"type": "polymer",
"material": "polyethylene naphthalate",
"code": "PEN",
"description": "Piringer parameters for PEN.",
"App": 5.0,
"tau": 1577
},
"PET": { # category: polyesters
"className": "gPET",
"type": "polymer",
"material": "glassy PET",
"code": "PET",
"description": "Piringer parameters for glassy PET (inf Tg).",
"App": 3.1,
"tau": 1577
},
"rPET": { # category: polyesters
"className": "rPET",
"type": "polymer",
"material": "rubbery PET",
"code": "rPET",
"description": "Piringer parameters for rubbery PET (sup Tg).",
"App": 6.4,
"tau": 1577
},
# -- polyamides --------------------------------------------
"PA6": { # category: polyamides
"className": "PA6",
"type": "polymer",
"material": "polyamide 6",
"code": "PA6",
"description": "Piringer parameters for polyamide 6.",
"App": 0.0,
"tau": 0
},
"PA6,6": { # category: polyamides
"className": "PA66",
"type": "polymer",
"material": "polyamide 6,6",
"code": "PA6,6",
"description": "Piringer parameters for polyamide 6,6.",
"App": 2.0,
"tau": 0
},
# -- adhesives --------------------------------------------
"Acryl": { # category: adhesives
"className": "AdhesiveAcrylate",
"type": "adhesive",
"material": "acrylate adhesive",
"code": "Acryl",
"description": "Piringer parameters for acrylate adhesive.",
"App": 4.5,
"tau": 83
},
"EVA": { # category: adhesives
"className": "AdhesiveEVA",
"type": "adhesive",
"material": "EVA adhesive",
"code": "EVA",
"description": "Piringer parameters for EVA adhesive.",
"App": 6.6,
"tau": -1270
},
"rubber": { # category: adhesives
"className": "AdhesiveNaturalRubber",
"type": "adhesive",
"material": "natural rubber adhesive",
"code": "rubber",
"description": "Piringer parameters for natural rubber adhesive.",
"App": 11.3,
"tau": -421
},
"PU": { # category: adhesives
"className": "AdhesivePU",
"type": "adhesive",
"material": "polyurethane adhesive",
"code": "PU",
"description": "Piringer parameters for polyurethane adhesive.",
"App": 4.0,
"tau": 250
},
"PVAc": { # category: adhesives
"className": "AdhesivePVAC",
"type": "adhesive",
"material": "PVAc adhesive",
"code": "PVAc",
"description": "Piringer parameters for PVAc adhesive.",
"App": 6.6,
"tau": -1270
},
"sRubber": { # category: adhesives
"className": "AdhesiveSyntheticRubber",
"type": "adhesive",
"material": "synthetic rubber adhesive",
"code": "sRubber",
"description": "Piringer parameters for synthetic rubber adhesive.",
"App": 11.3,
"tau": -421
},
"VAE": { # category: adhesives
"className": "AdhesiveVAE",
"type": "adhesive",
"material": "VAE adhesive",
"code": "VAE",
"description": "Piringer parameters for VAE adhesive.",
"App": 6.6,
"tau": -1270
},
# -- paper and board ---------------------------------------
"board_polar": { # category: paper_and_board
"className": "Cardboard",
"type": "paper",
"material": "cardboard",
"code": "board",
"description": "Piringer parameters for cardboard (polar migrants).",
"App": 4,
"tau": -1511
},
"board_apol": { # category: paper_and_board
"className": "Cardboard",
"type": "paper",
"material": "cardboard",
"code": "board",
"description": "Piringer parameters for cardboard (variant for apolar).",
"App": 7.4,
"tau": -1511
},
"paper": { # category: paper_and_board
"className": "Paper",
"type": "paper",
"material": "paper",
"code": "paper",
"description": "Piringer parameters for paper.",
"App": 6.6,
"tau": -1900
},
# -- air ----------------------------------------------------
"gas": { # category: air
"className": "air",
"type": "air",
"material": "ideal gas",
"code": "gas",
"description": "No Piringer data for air; set to None.",
"App": None,
"tau": None
}
}
def __init__(self, polymer="LDPE", M=100, T=40):
"""
Instantiate a Dpiringer object for a specific polymer key
(e.g. 'LDPE', 'PET', or 'air'). The corresponding App and tau
are looked up and stored as instance attributes.
"""
polymer_str = polymer.strip()
if polymer_str not in self.piringer_data:
print(f"No exact match for polymer key: {polymer_str!r}")
params = Dpiringer.get_piringer_params(polymer_str)
if params["App"] is None or params["tau"] is None:
raise ValueError(f"Piringer parameters not defined (App or tau is None) for {polymer_str!r}")
self._polymer = polymer_str
self._M = M
self._T = T
self._App = params["App"]
self._tau = params["tau"]
@property
def polymer(self) -> str:
"""Return the stored polymer code (e.g. 'PET')."""
return self._polymer
@property
def App(self) -> float:
"""Piringer's App constant for the selected polymer."""
return self._App
@property
def tau(self) -> float:
"""Piringer's tau constant for the selected polymer."""
return self._tau
@property
def M(self) -> float:
"""Molecular mass of the solute."""
return self._M
@property
def T(self) -> float:
"""Temperature in degC."""
return self._T
@M.setter
def M(self,value): self._M = value
@T.setter
def T(self,value): self._T = value
def eval(self, M=None, T=None, **extra):
"""
Compute Piringer D for this polymer (already stored in the instance)
at molecular mass M (g/mol) and temperature T (°C).
"""
M = self._M if M is None else M
T = self._T if T is None else T
# Convert T (°C) to T (K)
TK = T + 273.15
# Piringer expression for D in m^2/s
exponent = (self._App
- (self._tau / TK)
- 0.135 * (M ** (2.0 / 3.0))
+ 0.003 * M
- 10454.0 / TK)
return np.exp(exponent)
@classmethod
def get_piringer_params(cls,polymer: str, data: dict = piringer_data):
"""
Look up an entry in piringer_data by:
1) Dictionary key (e.g. "LDPE")
2) 'code' field (e.g. "LDPE")
3) 'className' field (e.g. "LDPE")
The matching is done case-insensitively.
- If an exact match is found in either of those fields, return that entry.
- If no exact match is found, attempt partial matches across all three fields and
display them in a neat Markdown table if multiple partial matches appear.
- If none found or the data is incomplete (App or tau is None), raise ValueError.
"""
if polymer is None:
raise ValueError("Please provide a polymer/material name")
if not isinstance(polymer,str):
raise TypeError(f"polymer must be a str not a {type(polymer).__name__}")
polymer_str = polymer.strip().lower()
# STEP 1: Try to find a single exact match
# across (dict key) or (entry["code"]) or (entry["className"])
matched_key = None
for k, info in data.items():
# Check dictionary key, code, className
if (
polymer_str == k.lower() or
(info["code"] and polymer_str == info["code"].lower()) or
(info["className"] and polymer_str == info["className"].lower())
):
matched_key = k
break
if matched_key is not None:
# We found an exact match.
entry = data[matched_key]
return entry
# STEP 2: No exact match => build partial match candidates
partial_matches = []
for k, info in data.items():
k_l = k.lower()
c_l = info["code"].lower() if info["code"] else ""
n_l = info["className"].lower() if info["className"] else ""
if (polymer_str in k_l) or (polymer_str in c_l) or (polymer_str in n_l):
partial_matches.append(k)
if not partial_matches:
# No partial matches
raise ValueError(f"No match or suggestion found for '{polymer}'.")
if len(partial_matches) == 1:
# Only one partial match => treat it like an exact match
matched_key = partial_matches[0]
entry = data[matched_key]
return entry
# STEP 3: Multiple partial matches => show a table
# We'll build a dynamic Markdown table with columns:
# Key | className | code | material
suggestions = []
for pm in partial_matches:
info = data[pm]
suggestions.append([
pm, info["className"], info["code"], info["material"]
])
# Headers
headers = ["Key", "className", "code", "material"]
# Find maximum width for each column
col_widths = [len(h) for h in headers]
for row in suggestions:
for i, cell in enumerate(row):
col_widths[i] = max(col_widths[i], len(cell))
# Build the header row
header_line = "| " + " | ".join(
headers[i].ljust(col_widths[i]) for i in range(len(headers))
) + " |"
# Separator
sep_line = "|-" + "-|-".join("-" * w for w in col_widths) + "-|"
# Rows
row_lines = []
for row in suggestions:
row_line = "| " + " | ".join(
row[i].ljust(col_widths[i]) for i in range(len(row))
) + " |"
row_lines.append(row_line)
markdown_table = "\n".join([header_line, sep_line] + row_lines)
raise ValueError(
f"No exact match found for '{polymer}'. "
f"Possible partial matches:\n\n{markdown_table}"
)
# static method (alternative for one shot evaluation)
@classmethod
def evaluate(cls, polymer="LLDPE", M=100.0, T=40.0, **extra):
"""
Evaluate D (Piringer) for a single polymer, molecular mass (M), and temperature (T in °C).
Replicates the essential logic of the original MATLAB Dpiringer function.
No vectorization is performed (handles one polymer at a time).
Parameters
----------
polymer : str
Polymer name (e.g. 'LLDPE', 'LDPE', 'rPET', etc.) as listed in the original data structure.
M : float
Molecular mass (g/mol), default = 100.
T : float
Temperature in °C, default = 40.
Returns
-------
float
The estimated diffusion coefficient in m^2/s (Piringer's overestimate).
"""
# get Piringer model parameter
params = cls.get_piringer_params(polymer)
if params["App"] is None or params["tau"] is None:
raise ValueError(
f"Data for '{polymer}' is incomplete: App or tau is None."
)
# Convert T (°C) to T (K)
TK = T + 273.15
# Compute Ap = App - tau / TK
App = params['App']
tau = params['tau']
Ap = App - tau / TK # dimensionless exponent part
# Piringer expression for D in m^2/s
# D = exp( Ap - 0.135 * M^(2/3) + 0.003 * M - 10454 / TK )
exponent = Ap - 0.135 * (M ** (2.0 / 3.0)) + 0.003 * M - 10454.0 / TK
D = np.exp(exponent)
return D
# %% Available models to expose to layer.py and food.py
# List below the importable models (currently only D, k, K models are possible)
# They are imported via:
# from property import MigrationPropertyModels, MigrationPropertyModel_validator
# Note that the name of the attribute (eg, "Piringer") must match class.name (eg, Dpiringer.name)
# A strict validator is proposed as MigrationPropertyModel_validator()
MigrationPropertyModels = {
"D":{
"Piringer": Dpiringer
},
"k":{
"kFHP": kFHP
},
"g":{
"gFHP": gFHP
},
"K":{
},
}
# Function helper to get a strict control on property models used by layer.py and food.py
def MigrationPropertyModel_validator(model=None,name=None,notation=None):
""" Returns True if the proposed model is valid for the requested migraton property """
rootclass = "migrationProperty"
expectedpropclass = {"D":"Diffusivities",
"k":"HenryLikeCoefficients",
"g":"ActivityCoefficients",
"K":"PartitionCoefficients"}
def get_root_parent(cls,level):
"""Returns the root parent class just after 'object'."""
mro = cls.mro() # Get the Method Resolution Order (MRO)
for base in mro[level:]: # Skip the class itself
if base is not object:
return base.__name__
return None # If no valid parent found
if model is None or name is None or notation is None:
raise ValueError("model, name and notation are mandatory.")
if notation not in MigrationPropertyModels:
raise ValueError(f"the property {notation} is not defined in MigrationPropertyModels")
if type(model).__name__!="type":
raise TypeError(f"model should be a class (e.g., Dpiringer) not a {type(model).__name__}")
if get_root_parent(model,2)!=rootclass:
raise TypeError(f'model "{model.__name__}" is not of class migrationProperty')
if get_root_parent(model,1)!=expectedpropclass[notation]:
raise TypeError(f'model "{model.__name__}" is not of class {expectedpropclass[notation]}, but of class {get_root_parent(model,1)}')
if not model._available_to_import:
raise TypeError(f'model "{model.__name__}" is not flagged for import')
if model.name!=name:
raise ValueError(f'model name "{model.name}" does not match the supplied name "{name}"')
if model.notation!=notation:
raise ValueError(f'model notation "{model.notation}" does not match the supplied name "{notation}"')
return True # if all tests passed
# %% debug and standalone
# -------------------------------------------------------------------
# Example usage (for debugging / standalone tests)
# -------------------------------------------------------------------
if __name__ == "__main__":
print(repr(Dpiringer()),"\n"*2)
# static use (without instantiation)
values = Dpiringer.get_piringer_params("air")
D = Dpiringer.evaluate("PET",100,40)
print(f"D1 = {D} [m**2/s]")
# dynamic use (with instantiation)
Dmodel = Dpiringer("PET",M=100,T=40)
Dvalue= Dmodel.eval()
print(f"D2 = {Dvalue} [m**2/s]")
Functions
def MigrationPropertyModel_validator(model=None, name=None, notation=None)
-
Returns True if the proposed model is valid for the requested migraton property
Expand source code
def MigrationPropertyModel_validator(model=None,name=None,notation=None): """ Returns True if the proposed model is valid for the requested migraton property """ rootclass = "migrationProperty" expectedpropclass = {"D":"Diffusivities", "k":"HenryLikeCoefficients", "g":"ActivityCoefficients", "K":"PartitionCoefficients"} def get_root_parent(cls,level): """Returns the root parent class just after 'object'.""" mro = cls.mro() # Get the Method Resolution Order (MRO) for base in mro[level:]: # Skip the class itself if base is not object: return base.__name__ return None # If no valid parent found if model is None or name is None or notation is None: raise ValueError("model, name and notation are mandatory.") if notation not in MigrationPropertyModels: raise ValueError(f"the property {notation} is not defined in MigrationPropertyModels") if type(model).__name__!="type": raise TypeError(f"model should be a class (e.g., Dpiringer) not a {type(model).__name__}") if get_root_parent(model,2)!=rootclass: raise TypeError(f'model "{model.__name__}" is not of class migrationProperty') if get_root_parent(model,1)!=expectedpropclass[notation]: raise TypeError(f'model "{model.__name__}" is not of class {expectedpropclass[notation]}, but of class {get_root_parent(model,1)}') if not model._available_to_import: raise TypeError(f'model "{model.__name__}" is not flagged for import') if model.name!=name: raise ValueError(f'model name "{model.name}" does not match the supplied name "{name}"') if model.notation!=notation: raise ValueError(f'model notation "{model.notation}" does not match the supplied name "{notation}"') return True # if all tests passed
Classes
class ActivityCoefficients
-
Base class to hold general properties used for migration of substances.
Expand source code
class ActivityCoefficients(migrationProperty): property = "Activity coefficient" notation = "g" description = "Mathematical model to estimate activity coefficients" SIunits = None
Ancestors
Subclasses
Class variables
var SIunits
var description
var notation
var property
class Diffusivities
-
Base class for diffusion-related models.
Expand source code
class Diffusivities(migrationProperty): """Base class for diffusion-related models.""" property = "Diffusivity" notation = "D" description = "Mathematical model to estimate diffusivities" SIunits = "m**2/s"
Ancestors
Subclasses
Class variables
var SIunits
var description
var notation
var property
class Dpiringer (polymer='LDPE', M=100, T=40)
-
Piringer's overestimate of diffusion coefficient.
Two implementations are offered in the class: - static: Dpiringer.evaluate(polymer="polymer",M=Mvalue,T=Tvalue) - dynamic: Dmodel = Dpiringer(polymer="polymer"…) Dmodel.eval(M=Mvalue,T=Tvalue)
Instantiate a Dpiringer object for a specific polymer key (e.g. 'LDPE', 'PET', or 'air'). The corresponding App and tau are looked up and stored as instance attributes.
Expand source code
class Dpiringer(Diffusivities): """ Piringer's overestimate of diffusion coefficient. Two implementations are offered in the class: - static: Dpiringer.evaluate(polymer="polymer",M=Mvalue,T=Tvalue) - dynamic: Dmodel = Dpiringer(polymer="polymer"...) Dmodel.eval(M=Mvalue,T=Tvalue) """ name = "Piringer" description = "Piringer's overestimate of diffusion coefficients" model = "empirical" theory = "scaling" parameters = {"M": {"description": "molecular mass","units": "g/mol"}, "T": {"description": "temperature","units": "degC"} } _available_to_import = True # this model can be directly imported # Piringer values (the primary key matches the one used in layer) piringer_data = { # -- polyolefins ------------------------------------------- "HDPE": { # category: polyolefins "className": "HDPE", "type": "polymer", "material": "high-density polyethylene", "code": "HDPE", "description": "Piringer parameters for HDPE.", "App": 14.5, "tau": 1577 }, "LDPE": { # category: polyolefins "className": "LDPE", "type": "polymer", "material": "low-density polyethylene", "code": "LDPE", "description": "Piringer parameters for LDPE.", "App": 11.5, "tau": 0 }, "LLDPE": { # category: polyolefins "className": "LLDPE", "type": "polymer", "material": "linear low-density polyethylene", "code": "LLDPE", "description": "Piringer parameters for LLDPE.", "App": 11.5, "tau": 0 }, "PP": { # category: polyolefins "className": "PP", "type": "polymer", "material": "isotactic polypropylene", "code": "PP", "description": "Piringer parameters for isotactic PP.", "App": 13.1, "tau": 1577 }, "aPP": { # category: polyolefins "className": "PPrubber", "type": "polymer", "material": "atactic polypropylene", "code": "aPP", "description": "Piringer parameters for atactic PP.", "App": 11.5, "tau": 0 }, "oPP": { # category: polyolefins "className": "oPP", "type": "polymer", "material": "bioriented polypropylene", "code": "oPP", "description": "Piringer parameters for bioriented PP.", "App": 13.1, "tau": 1577 }, # -- polyvinyls -------------------------------------------- "pPVC": { # category: polyvinyls "className": "plasticizedPVC", "type": "polymer", "material": "plasticized PVC", "code": "pPVC", "description": "Piringer parameters for plasticized PVC.", "App": 14.6, "tau": 0 }, "PVC": { # category: polyvinyls "className": "rigidPVC", "type": "polymer", "material": "rigid PVC", "code": "PVC", "description": "Piringer parameters for rigid PVC.", "App": -1.0, "tau": 0 }, # -- polystyrene, etc. (misc) ------------------------------ "HIPS": { # category: polystyrenics "className": "HIPS", "type": "polymer", "material": "high-impact polystyrene", "code": "HIPS", "description": "Piringer parameters for HIPS.", "App": 1.0, "tau": 0 }, "PBS": { # category: polystyrenics "className": "PBS", "type": "polymer", "material": "styrene-based polymer PBS", "code": "PBS", "description": "No original Piringer data; set to None.", "App": 10.5, "tau": 0 }, "PS": { # category: polystyrenics "className": "PS", "type": "polymer", "material": "polystyrene", "code": "PS", "description": "Piringer parameters for PS.", "App": -1.0, "tau": 0 }, # -- polyesters -------------------------------------------- "PBT": { # category: polyesters "className": "PBT", "type": "polymer", "material": "polybutylene terephthalate", "code": "PBT", "description": "Piringer parameters for PBT.", "App": 6.5, "tau": 1577 }, "PEN": { # category: polyesters "className": "PEN", "type": "polymer", "material": "polyethylene naphthalate", "code": "PEN", "description": "Piringer parameters for PEN.", "App": 5.0, "tau": 1577 }, "PET": { # category: polyesters "className": "gPET", "type": "polymer", "material": "glassy PET", "code": "PET", "description": "Piringer parameters for glassy PET (inf Tg).", "App": 3.1, "tau": 1577 }, "rPET": { # category: polyesters "className": "rPET", "type": "polymer", "material": "rubbery PET", "code": "rPET", "description": "Piringer parameters for rubbery PET (sup Tg).", "App": 6.4, "tau": 1577 }, # -- polyamides -------------------------------------------- "PA6": { # category: polyamides "className": "PA6", "type": "polymer", "material": "polyamide 6", "code": "PA6", "description": "Piringer parameters for polyamide 6.", "App": 0.0, "tau": 0 }, "PA6,6": { # category: polyamides "className": "PA66", "type": "polymer", "material": "polyamide 6,6", "code": "PA6,6", "description": "Piringer parameters for polyamide 6,6.", "App": 2.0, "tau": 0 }, # -- adhesives -------------------------------------------- "Acryl": { # category: adhesives "className": "AdhesiveAcrylate", "type": "adhesive", "material": "acrylate adhesive", "code": "Acryl", "description": "Piringer parameters for acrylate adhesive.", "App": 4.5, "tau": 83 }, "EVA": { # category: adhesives "className": "AdhesiveEVA", "type": "adhesive", "material": "EVA adhesive", "code": "EVA", "description": "Piringer parameters for EVA adhesive.", "App": 6.6, "tau": -1270 }, "rubber": { # category: adhesives "className": "AdhesiveNaturalRubber", "type": "adhesive", "material": "natural rubber adhesive", "code": "rubber", "description": "Piringer parameters for natural rubber adhesive.", "App": 11.3, "tau": -421 }, "PU": { # category: adhesives "className": "AdhesivePU", "type": "adhesive", "material": "polyurethane adhesive", "code": "PU", "description": "Piringer parameters for polyurethane adhesive.", "App": 4.0, "tau": 250 }, "PVAc": { # category: adhesives "className": "AdhesivePVAC", "type": "adhesive", "material": "PVAc adhesive", "code": "PVAc", "description": "Piringer parameters for PVAc adhesive.", "App": 6.6, "tau": -1270 }, "sRubber": { # category: adhesives "className": "AdhesiveSyntheticRubber", "type": "adhesive", "material": "synthetic rubber adhesive", "code": "sRubber", "description": "Piringer parameters for synthetic rubber adhesive.", "App": 11.3, "tau": -421 }, "VAE": { # category: adhesives "className": "AdhesiveVAE", "type": "adhesive", "material": "VAE adhesive", "code": "VAE", "description": "Piringer parameters for VAE adhesive.", "App": 6.6, "tau": -1270 }, # -- paper and board --------------------------------------- "board_polar": { # category: paper_and_board "className": "Cardboard", "type": "paper", "material": "cardboard", "code": "board", "description": "Piringer parameters for cardboard (polar migrants).", "App": 4, "tau": -1511 }, "board_apol": { # category: paper_and_board "className": "Cardboard", "type": "paper", "material": "cardboard", "code": "board", "description": "Piringer parameters for cardboard (variant for apolar).", "App": 7.4, "tau": -1511 }, "paper": { # category: paper_and_board "className": "Paper", "type": "paper", "material": "paper", "code": "paper", "description": "Piringer parameters for paper.", "App": 6.6, "tau": -1900 }, # -- air ---------------------------------------------------- "gas": { # category: air "className": "air", "type": "air", "material": "ideal gas", "code": "gas", "description": "No Piringer data for air; set to None.", "App": None, "tau": None } } def __init__(self, polymer="LDPE", M=100, T=40): """ Instantiate a Dpiringer object for a specific polymer key (e.g. 'LDPE', 'PET', or 'air'). The corresponding App and tau are looked up and stored as instance attributes. """ polymer_str = polymer.strip() if polymer_str not in self.piringer_data: print(f"No exact match for polymer key: {polymer_str!r}") params = Dpiringer.get_piringer_params(polymer_str) if params["App"] is None or params["tau"] is None: raise ValueError(f"Piringer parameters not defined (App or tau is None) for {polymer_str!r}") self._polymer = polymer_str self._M = M self._T = T self._App = params["App"] self._tau = params["tau"] @property def polymer(self) -> str: """Return the stored polymer code (e.g. 'PET').""" return self._polymer @property def App(self) -> float: """Piringer's App constant for the selected polymer.""" return self._App @property def tau(self) -> float: """Piringer's tau constant for the selected polymer.""" return self._tau @property def M(self) -> float: """Molecular mass of the solute.""" return self._M @property def T(self) -> float: """Temperature in degC.""" return self._T @M.setter def M(self,value): self._M = value @T.setter def T(self,value): self._T = value def eval(self, M=None, T=None, **extra): """ Compute Piringer D for this polymer (already stored in the instance) at molecular mass M (g/mol) and temperature T (°C). """ M = self._M if M is None else M T = self._T if T is None else T # Convert T (°C) to T (K) TK = T + 273.15 # Piringer expression for D in m^2/s exponent = (self._App - (self._tau / TK) - 0.135 * (M ** (2.0 / 3.0)) + 0.003 * M - 10454.0 / TK) return np.exp(exponent) @classmethod def get_piringer_params(cls,polymer: str, data: dict = piringer_data): """ Look up an entry in piringer_data by: 1) Dictionary key (e.g. "LDPE") 2) 'code' field (e.g. "LDPE") 3) 'className' field (e.g. "LDPE") The matching is done case-insensitively. - If an exact match is found in either of those fields, return that entry. - If no exact match is found, attempt partial matches across all three fields and display them in a neat Markdown table if multiple partial matches appear. - If none found or the data is incomplete (App or tau is None), raise ValueError. """ if polymer is None: raise ValueError("Please provide a polymer/material name") if not isinstance(polymer,str): raise TypeError(f"polymer must be a str not a {type(polymer).__name__}") polymer_str = polymer.strip().lower() # STEP 1: Try to find a single exact match # across (dict key) or (entry["code"]) or (entry["className"]) matched_key = None for k, info in data.items(): # Check dictionary key, code, className if ( polymer_str == k.lower() or (info["code"] and polymer_str == info["code"].lower()) or (info["className"] and polymer_str == info["className"].lower()) ): matched_key = k break if matched_key is not None: # We found an exact match. entry = data[matched_key] return entry # STEP 2: No exact match => build partial match candidates partial_matches = [] for k, info in data.items(): k_l = k.lower() c_l = info["code"].lower() if info["code"] else "" n_l = info["className"].lower() if info["className"] else "" if (polymer_str in k_l) or (polymer_str in c_l) or (polymer_str in n_l): partial_matches.append(k) if not partial_matches: # No partial matches raise ValueError(f"No match or suggestion found for '{polymer}'.") if len(partial_matches) == 1: # Only one partial match => treat it like an exact match matched_key = partial_matches[0] entry = data[matched_key] return entry # STEP 3: Multiple partial matches => show a table # We'll build a dynamic Markdown table with columns: # Key | className | code | material suggestions = [] for pm in partial_matches: info = data[pm] suggestions.append([ pm, info["className"], info["code"], info["material"] ]) # Headers headers = ["Key", "className", "code", "material"] # Find maximum width for each column col_widths = [len(h) for h in headers] for row in suggestions: for i, cell in enumerate(row): col_widths[i] = max(col_widths[i], len(cell)) # Build the header row header_line = "| " + " | ".join( headers[i].ljust(col_widths[i]) for i in range(len(headers)) ) + " |" # Separator sep_line = "|-" + "-|-".join("-" * w for w in col_widths) + "-|" # Rows row_lines = [] for row in suggestions: row_line = "| " + " | ".join( row[i].ljust(col_widths[i]) for i in range(len(row)) ) + " |" row_lines.append(row_line) markdown_table = "\n".join([header_line, sep_line] + row_lines) raise ValueError( f"No exact match found for '{polymer}'. " f"Possible partial matches:\n\n{markdown_table}" ) # static method (alternative for one shot evaluation) @classmethod def evaluate(cls, polymer="LLDPE", M=100.0, T=40.0, **extra): """ Evaluate D (Piringer) for a single polymer, molecular mass (M), and temperature (T in °C). Replicates the essential logic of the original MATLAB Dpiringer function. No vectorization is performed (handles one polymer at a time). Parameters ---------- polymer : str Polymer name (e.g. 'LLDPE', 'LDPE', 'rPET', etc.) as listed in the original data structure. M : float Molecular mass (g/mol), default = 100. T : float Temperature in °C, default = 40. Returns ------- float The estimated diffusion coefficient in m^2/s (Piringer's overestimate). """ # get Piringer model parameter params = cls.get_piringer_params(polymer) if params["App"] is None or params["tau"] is None: raise ValueError( f"Data for '{polymer}' is incomplete: App or tau is None." ) # Convert T (°C) to T (K) TK = T + 273.15 # Compute Ap = App - tau / TK App = params['App'] tau = params['tau'] Ap = App - tau / TK # dimensionless exponent part # Piringer expression for D in m^2/s # D = exp( Ap - 0.135 * M^(2/3) + 0.003 * M - 10454 / TK ) exponent = Ap - 0.135 * (M ** (2.0 / 3.0)) + 0.003 * M - 10454.0 / TK D = np.exp(exponent) return D
Ancestors
Class variables
var description
var model
var name
var parameters
var piringer_data
var theory
Static methods
def evaluate(polymer='LLDPE', M=100.0, T=40.0, **extra)
-
Evaluate D (Piringer) for a single polymer, molecular mass (M), and temperature (T in °C). Replicates the essential logic of the original MATLAB Dpiringer function. No vectorization is performed (handles one polymer at a time).
Parameters
polymer
:str
- Polymer name (e.g. 'LLDPE', 'LDPE', 'rPET', etc.) as listed in the original data structure.
M
:float
- Molecular mass (g/mol), default = 100.
T
:float
- Temperature in °C, default = 40.
Returns
float
- The estimated diffusion coefficient in m^2/s (Piringer's overestimate).
Expand source code
@classmethod def evaluate(cls, polymer="LLDPE", M=100.0, T=40.0, **extra): """ Evaluate D (Piringer) for a single polymer, molecular mass (M), and temperature (T in °C). Replicates the essential logic of the original MATLAB Dpiringer function. No vectorization is performed (handles one polymer at a time). Parameters ---------- polymer : str Polymer name (e.g. 'LLDPE', 'LDPE', 'rPET', etc.) as listed in the original data structure. M : float Molecular mass (g/mol), default = 100. T : float Temperature in °C, default = 40. Returns ------- float The estimated diffusion coefficient in m^2/s (Piringer's overestimate). """ # get Piringer model parameter params = cls.get_piringer_params(polymer) if params["App"] is None or params["tau"] is None: raise ValueError( f"Data for '{polymer}' is incomplete: App or tau is None." ) # Convert T (°C) to T (K) TK = T + 273.15 # Compute Ap = App - tau / TK App = params['App'] tau = params['tau'] Ap = App - tau / TK # dimensionless exponent part # Piringer expression for D in m^2/s # D = exp( Ap - 0.135 * M^(2/3) + 0.003 * M - 10454 / TK ) exponent = Ap - 0.135 * (M ** (2.0 / 3.0)) + 0.003 * M - 10454.0 / TK D = np.exp(exponent) return D
def get_piringer_params(polymer: str, data: dict = {'HDPE': {'className': 'HDPE', 'type': 'polymer', 'material': 'high-density polyethylene', 'code': 'HDPE', 'description': 'Piringer parameters for HDPE.', 'App': 14.5, 'tau': 1577}, 'LDPE': {'className': 'LDPE', 'type': 'polymer', 'material': 'low-density polyethylene', 'code': 'LDPE', 'description': 'Piringer parameters for LDPE.', 'App': 11.5, 'tau': 0}, 'LLDPE': {'className': 'LLDPE', 'type': 'polymer', 'material': 'linear low-density polyethylene', 'code': 'LLDPE', 'description': 'Piringer parameters for LLDPE.', 'App': 11.5, 'tau': 0}, 'PP': {'className': 'PP', 'type': 'polymer', 'material': 'isotactic polypropylene', 'code': 'PP', 'description': 'Piringer parameters for isotactic PP.', 'App': 13.1, 'tau': 1577}, 'aPP': {'className': 'PPrubber', 'type': 'polymer', 'material': 'atactic polypropylene', 'code': 'aPP', 'description': 'Piringer parameters for atactic PP.', 'App': 11.5, 'tau': 0}, 'oPP': {'className': 'oPP', 'type': 'polymer', 'material': 'bioriented polypropylene', 'code': 'oPP', 'description': 'Piringer parameters for bioriented PP.', 'App': 13.1, 'tau': 1577}, 'pPVC': {'className': 'plasticizedPVC', 'type': 'polymer', 'material': 'plasticized PVC', 'code': 'pPVC', 'description': 'Piringer parameters for plasticized PVC.', 'App': 14.6, 'tau': 0}, 'PVC': {'className': 'rigidPVC', 'type': 'polymer', 'material': 'rigid PVC', 'code': 'PVC', 'description': 'Piringer parameters for rigid PVC.', 'App': -1.0, 'tau': 0}, 'HIPS': {'className': 'HIPS', 'type': 'polymer', 'material': 'high-impact polystyrene', 'code': 'HIPS', 'description': 'Piringer parameters for HIPS.', 'App': 1.0, 'tau': 0}, 'PBS': {'className': 'PBS', 'type': 'polymer', 'material': 'styrene-based polymer PBS', 'code': 'PBS', 'description': 'No original Piringer data; set to None.', 'App': 10.5, 'tau': 0}, 'PS': {'className': 'PS', 'type': 'polymer', 'material': 'polystyrene', 'code': 'PS', 'description': 'Piringer parameters for PS.', 'App': -1.0, 'tau': 0}, 'PBT': {'className': 'PBT', 'type': 'polymer', 'material': 'polybutylene terephthalate', 'code': 'PBT', 'description': 'Piringer parameters for PBT.', 'App': 6.5, 'tau': 1577}, 'PEN': {'className': 'PEN', 'type': 'polymer', 'material': 'polyethylene naphthalate', 'code': 'PEN', 'description': 'Piringer parameters for PEN.', 'App': 5.0, 'tau': 1577}, 'PET': {'className': 'gPET', 'type': 'polymer', 'material': 'glassy PET', 'code': 'PET', 'description': 'Piringer parameters for glassy PET (inf Tg).', 'App': 3.1, 'tau': 1577}, 'rPET': {'className': 'rPET', 'type': 'polymer', 'material': 'rubbery PET', 'code': 'rPET', 'description': 'Piringer parameters for rubbery PET (sup Tg).', 'App': 6.4, 'tau': 1577}, 'PA6': {'className': 'PA6', 'type': 'polymer', 'material': 'polyamide 6', 'code': 'PA6', 'description': 'Piringer parameters for polyamide 6.', 'App': 0.0, 'tau': 0}, 'PA6,6': {'className': 'PA66', 'type': 'polymer', 'material': 'polyamide 6,6', 'code': 'PA6,6', 'description': 'Piringer parameters for polyamide 6,6.', 'App': 2.0, 'tau': 0}, 'Acryl': {'className': 'AdhesiveAcrylate', 'type': 'adhesive', 'material': 'acrylate adhesive', 'code': 'Acryl', 'description': 'Piringer parameters for acrylate adhesive.', 'App': 4.5, 'tau': 83}, 'EVA': {'className': 'AdhesiveEVA', 'type': 'adhesive', 'material': 'EVA adhesive', 'code': 'EVA', 'description': 'Piringer parameters for EVA adhesive.', 'App': 6.6, 'tau': -1270}, 'rubber': {'className': 'AdhesiveNaturalRubber', 'type': 'adhesive', 'material': 'natural rubber adhesive', 'code': 'rubber', 'description': 'Piringer parameters for natural rubber adhesive.', 'App': 11.3, 'tau': -421}, 'PU': {'className': 'AdhesivePU', 'type': 'adhesive', 'material': 'polyurethane adhesive', 'code': 'PU', 'description': 'Piringer parameters for polyurethane adhesive.', 'App': 4.0, 'tau': 250}, 'PVAc': {'className': 'AdhesivePVAC', 'type': 'adhesive', 'material': 'PVAc adhesive', 'code': 'PVAc', 'description': 'Piringer parameters for PVAc adhesive.', 'App': 6.6, 'tau': -1270}, 'sRubber': {'className': 'AdhesiveSyntheticRubber', 'type': 'adhesive', 'material': 'synthetic rubber adhesive', 'code': 'sRubber', 'description': 'Piringer parameters for synthetic rubber adhesive.', 'App': 11.3, 'tau': -421}, 'VAE': {'className': 'AdhesiveVAE', 'type': 'adhesive', 'material': 'VAE adhesive', 'code': 'VAE', 'description': 'Piringer parameters for VAE adhesive.', 'App': 6.6, 'tau': -1270}, 'board_polar': {'className': 'Cardboard', 'type': 'paper', 'material': 'cardboard', 'code': 'board', 'description': 'Piringer parameters for cardboard (polar migrants).', 'App': 4, 'tau': -1511}, 'board_apol': {'className': 'Cardboard', 'type': 'paper', 'material': 'cardboard', 'code': 'board', 'description': 'Piringer parameters for cardboard (variant for apolar).', 'App': 7.4, 'tau': -1511}, 'paper': {'className': 'Paper', 'type': 'paper', 'material': 'paper', 'code': 'paper', 'description': 'Piringer parameters for paper.', 'App': 6.6, 'tau': -1900}, 'gas': {'className': 'air', 'type': 'air', 'material': 'ideal gas', 'code': 'gas', 'description': 'No Piringer data for air; set to None.', 'App': None, 'tau': None}})
-
Look up an entry in piringer_data by: 1) Dictionary key (e.g. "LDPE") 2) 'code' field (e.g. "LDPE") 3) 'className' field (e.g. "LDPE")
The matching is done case-insensitively.
- If an exact match is found in either of those fields, return that entry.
- If no exact match is found, attempt partial matches across all three fields and display them in a neat Markdown table if multiple partial matches appear.
- If none found or the data is incomplete (App or tau is None), raise ValueError.
Expand source code
@classmethod def get_piringer_params(cls,polymer: str, data: dict = piringer_data): """ Look up an entry in piringer_data by: 1) Dictionary key (e.g. "LDPE") 2) 'code' field (e.g. "LDPE") 3) 'className' field (e.g. "LDPE") The matching is done case-insensitively. - If an exact match is found in either of those fields, return that entry. - If no exact match is found, attempt partial matches across all three fields and display them in a neat Markdown table if multiple partial matches appear. - If none found or the data is incomplete (App or tau is None), raise ValueError. """ if polymer is None: raise ValueError("Please provide a polymer/material name") if not isinstance(polymer,str): raise TypeError(f"polymer must be a str not a {type(polymer).__name__}") polymer_str = polymer.strip().lower() # STEP 1: Try to find a single exact match # across (dict key) or (entry["code"]) or (entry["className"]) matched_key = None for k, info in data.items(): # Check dictionary key, code, className if ( polymer_str == k.lower() or (info["code"] and polymer_str == info["code"].lower()) or (info["className"] and polymer_str == info["className"].lower()) ): matched_key = k break if matched_key is not None: # We found an exact match. entry = data[matched_key] return entry # STEP 2: No exact match => build partial match candidates partial_matches = [] for k, info in data.items(): k_l = k.lower() c_l = info["code"].lower() if info["code"] else "" n_l = info["className"].lower() if info["className"] else "" if (polymer_str in k_l) or (polymer_str in c_l) or (polymer_str in n_l): partial_matches.append(k) if not partial_matches: # No partial matches raise ValueError(f"No match or suggestion found for '{polymer}'.") if len(partial_matches) == 1: # Only one partial match => treat it like an exact match matched_key = partial_matches[0] entry = data[matched_key] return entry # STEP 3: Multiple partial matches => show a table # We'll build a dynamic Markdown table with columns: # Key | className | code | material suggestions = [] for pm in partial_matches: info = data[pm] suggestions.append([ pm, info["className"], info["code"], info["material"] ]) # Headers headers = ["Key", "className", "code", "material"] # Find maximum width for each column col_widths = [len(h) for h in headers] for row in suggestions: for i, cell in enumerate(row): col_widths[i] = max(col_widths[i], len(cell)) # Build the header row header_line = "| " + " | ".join( headers[i].ljust(col_widths[i]) for i in range(len(headers)) ) + " |" # Separator sep_line = "|-" + "-|-".join("-" * w for w in col_widths) + "-|" # Rows row_lines = [] for row in suggestions: row_line = "| " + " | ".join( row[i].ljust(col_widths[i]) for i in range(len(row)) ) + " |" row_lines.append(row_line) markdown_table = "\n".join([header_line, sep_line] + row_lines) raise ValueError( f"No exact match found for '{polymer}'. " f"Possible partial matches:\n\n{markdown_table}" )
Instance variables
var App : float
-
Piringer's App constant for the selected polymer.
Expand source code
@property def App(self) -> float: """Piringer's App constant for the selected polymer.""" return self._App
var M : float
-
Molecular mass of the solute.
Expand source code
@property def M(self) -> float: """Molecular mass of the solute.""" return self._M
var T : float
-
Temperature in degC.
Expand source code
@property def T(self) -> float: """Temperature in degC.""" return self._T
var polymer : str
-
Return the stored polymer code (e.g. 'PET').
Expand source code
@property def polymer(self) -> str: """Return the stored polymer code (e.g. 'PET').""" return self._polymer
var tau : float
-
Piringer's tau constant for the selected polymer.
Expand source code
@property def tau(self) -> float: """Piringer's tau constant for the selected polymer.""" return self._tau
Methods
def eval(self, M=None, T=None, **extra)
-
Compute Piringer D for this polymer (already stored in the instance) at molecular mass M (g/mol) and temperature T (°C).
Expand source code
def eval(self, M=None, T=None, **extra): """ Compute Piringer D for this polymer (already stored in the instance) at molecular mass M (g/mol) and temperature T (°C). """ M = self._M if M is None else M T = self._T if T is None else T # Convert T (°C) to T (K) TK = T + 273.15 # Piringer expression for D in m^2/s exponent = (self._App - (self._tau / TK) - 0.135 * (M ** (2.0 / 3.0)) + 0.003 * M - 10454.0 / TK) return np.exp(exponent)
class HenryLikeCoefficients
-
Base class to hold general properties used for migration of substances.
Expand source code
class HenryLikeCoefficients(migrationProperty): property = "Henri-like coefficient" notation = "k" description = "Mathematical model to estimate Henri-like coefficients" SIunits = None
Ancestors
Subclasses
Class variables
var SIunits
var description
var notation
var property
class PartitionCoeffcicients
-
Base class to hold general properties used for migration of substances.
Expand source code
class PartitionCoeffcicients(migrationProperty): property = "Partition Coefficient" notation = "K" description = "Mathematical model to estimate partition coefficients" SIunits = None
Ancestors
Class variables
var SIunits
var description
var notation
var property
class gFHP
-
Simplified model to estimate activity coefficients gik from P'i, P'k, Vi, Vk i: solute k: P or F P'i and P'k: Polarity index (e.g.: migrant("solute").polarityindex) Vi, Vk: molar volumes (e.g. migrant("solute").molarvolumeMiller) ispolymer: True for polymers alpha: scaling constant for chiik (default=0.14=1/migrant("water").polarityindex) lngmin: minimum value (default=0) gscale: activity coefficient (default=1.0)
Use gscale to enforce gik<=1
Only a static evaluate is proposed.
Expand source code
class gFHP(ActivityCoefficients): """ Simplified model to estimate activity coefficients gik from P'i, P'k, Vi, Vk i: solute k: P or F P'i and P'k: Polarity index (e.g.: migrant("solute").polarityindex) Vi, Vk: molar volumes (e.g. migrant("solute").molarvolumeMiller) ispolymer: True for polymers alpha: scaling constant for chiik (default=0.14=1/migrant("water").polarityindex) lngmin: minimum value (default=0) gscale: activity coefficient (default=1.0) Use gscale to enforce gik<=1 Only a static evaluate is proposed. """ name = "gFHP" description = "Flory-Huggins model of activity coefficients from P' and V at infinite dilution in k" model = "semi-empirical" theory = "Flory-Huggins" parameters = {"Pi": {"description": "polarity index of solute i","units": "-"}, "Pk": {"description": "polarity index of continuous phase k","units": "-"}, "Vi": {"description": "molar volume of i","units": "-"}, "Vk": {"description": "molar volume of k","units": "-"} } _available_to_import = True # this model can be imported @classmethod def evaluate(cls, Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer = False, alpha=0.14,lngmin=0.0,gscale=1.0): """evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)""" if ispolymer: rik = 0.0 nik = 0.0 chimin = 0.25 else: rik = Vi/Vk nik = (rik - 5)/5 chimin = 0 chiik = np.maximum(chimin,alpha * (Pi - Pk)**2) lngik = np.maximum(lngmin,chiik + 1 - (rik - nik)) return gscale * np.exp(lngik)
Ancestors
Class variables
var description
var model
var name
var parameters
var theory
Static methods
def evaluate(Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer=False, alpha=0.14, lngmin=0.0, gscale=1.0)
-
evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)
Expand source code
@classmethod def evaluate(cls, Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer = False, alpha=0.14,lngmin=0.0,gscale=1.0): """evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)""" if ispolymer: rik = 0.0 nik = 0.0 chimin = 0.25 else: rik = Vi/Vk nik = (rik - 5)/5 chimin = 0 chiik = np.maximum(chimin,alpha * (Pi - Pk)**2) lngik = np.maximum(lngmin,chiik + 1 - (rik - nik)) return gscale * np.exp(lngik)
class kFHP
-
Simplified model to estimate Henry-like coefficients based on gFHP class ki,k = Vi * Pi gik(P'i,P'k,Vi,Vk)
i: solute k: P or F P'i and P'k: Polarity index (e.g.: migrant("solute").polarityindex) Vi, Vk: molar volumes (e.g. migrant("solute").molarvolumeMiller) ispolymer: True for polymers alpha: scaling constant for chiik (default=0.14=1/migrant("water").polarityindex) lngmin: minimum value (default=0) gscale: activity coefficient (default=Vi*Psat) Psat: vapor saturation pressure
Only a static evaluate is proposed.
Note use: scaling = False to get activity coefficients instead of Henry-like ones
Expand source code
class kFHP(HenryLikeCoefficients): """ Simplified model to estimate Henry-like coefficients based on gFHP class ki,k = Vi * Pi gik(P'i,P'k,Vi,Vk) i: solute k: P or F P'i and P'k: Polarity index (e.g.: migrant("solute").polarityindex) Vi, Vk: molar volumes (e.g. migrant("solute").molarvolumeMiller) ispolymer: True for polymers alpha: scaling constant for chiik (default=0.14=1/migrant("water").polarityindex) lngmin: minimum value (default=0) gscale: activity coefficient (default=Vi*Psat) Psat: vapor saturation pressure Only a static evaluate is proposed. Note use: scaling = False to get activity coefficients instead of Henry-like ones """ name = "kFHP" description = "Flory-Huggins model of Henry-likecoefficients from P' and V at infinite dilution in k" model = "semi-empirical" theory = "Flory-Huggins" parameters = {"Pi": {"description": "polarity index of solute i","units": "-"}, "Pk": {"description": "polarity index of continuous phase k","units": "-"}, "Vi": {"description": "molar volume","units": "g/cm**3"}, "Vk": {"description": "molar volume of k","units": "g/cm**3"}, "Psat": {"description": "vapor saturation pressure of i","units": "Pa"} } _available_to_import = True # this model can be imported @classmethod def evaluate(cls, Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer = False, alpha=0.14,lngmin=0.0,Psat=1.0,scaling=True): """evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)""" if scaling: # (default behavior) return gFHP.evaluate(Pi=Pi, Pk=Pk, Vi=Vi, Vk=Vk, ispolymer=ispolymer, alpha=alpha,lngmin=lngmin, gscale=Vi*1e-3*Psat # Vi is converted [cm**3/g] --> [m**3/kg] ) else: return gFHP.evaluate(Pi=Pi, Pk=Pk, Vi=Vi, Vk=Vk, ispolymer=ispolymer, alpha=alpha,lngmin=lngmin, gscale=1 )
Ancestors
Class variables
var description
var model
var name
var parameters
var theory
Static methods
def evaluate(Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer=False, alpha=0.14, lngmin=0.0, Psat=1.0, scaling=True)
-
evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)
Expand source code
@classmethod def evaluate(cls, Pi=1.41, Pk=3.97, Vi=124.1, Vk=30.9, ispolymer = False, alpha=0.14,lngmin=0.0,Psat=1.0,scaling=True): """evaluate gFHP model(Pi,Pk,Vi,Vk,ispolymer)""" if scaling: # (default behavior) return gFHP.evaluate(Pi=Pi, Pk=Pk, Vi=Vi, Vk=Vk, ispolymer=ispolymer, alpha=alpha,lngmin=lngmin, gscale=Vi*1e-3*Psat # Vi is converted [cm**3/g] --> [m**3/kg] ) else: return gFHP.evaluate(Pi=Pi, Pk=Pk, Vi=Vi, Vk=Vk, ispolymer=ispolymer, alpha=alpha,lngmin=lngmin, gscale=1 )
class migrationProperty
-
Base class to hold general properties used for migration of substances.
Expand source code
class migrationProperty: """Base class to hold general properties used for migration of substances.""" property = "any" notation = "" description = "root class" name = "root" parameters = [] # e.g. ["M", "T"] SIunits = "" # private properties _model = "" _theory = "" _source = "" _author = "olivier.vitrac@agroparistech.fr" _license = "MIT" _version = 1.21 _available_to_import = False def __repr__(self): """Formatted string representation for nice display.""" # Define attribute names and their corresponding values attributes = { "property": self.property, "notation": self.notation, "description": self.description, "name": self.name, "parameters": self.parameters, "SIunits": self.SIunits, "model": self._model, "theory": self._theory, "source": self._source, "author": self._author, "license": self._license, "version": self._version, } # Filter out None or empty string values filtered_attributes = {k: v for k, v in attributes.items() if v not in (None, "")} # Find the max length of attribute names for alignment max_key_length = max(len(k) for k in filtered_attributes.keys()) if filtered_attributes else 0 # Format the output with proper alignment lines = [f"{k.rjust(max_key_length)}: {v}" for k, v in filtered_attributes.items()] print("\n".join(lines)) return str(self) def __str__(self): """Formatted string representation of property""" return f"<{self.__class__.__name__}: {self.property}:{self.notation}>"
Subclasses
Class variables
var SIunits
var description
var name
var notation
var parameters
var property