Module layer
===============================================================================
SFPPy Module: Layer (Packaging Materials)
===============================================================================
Defines packaging materials as 1D layers. Supports:
- Multilayer assembly (layer1 + layer2
)
- Mass transfer modeling (layer >> food
)
- Automatic meshing for finite-volume solvers
Main Components:
- Base Class: layer
(Defines all packaging materials)
- Properties: D
(diffusivity), k
(partition coefficient), l
(thickness)
- Supports + (stacking) and splitting layers
- Propagates contact temperature from food.py
- Predefined Materials (Subclasses):
- LDPE
, PP
, PET
, Cardboard
, Ink
- Dynamic Property Models:
- Dmodel()
, kmodel()
: Call property.py
to predict diffusion and partitioning
Integration with SFPPy Modules:
- Used in migration.py
to define the left-side boundary.
- Retrieves chemical properties from loadpubchem.py
.
- Works with food.py
to model food-contact interactions.
Example:
from patankar.layer import LDPE
A = LDPE(l=50e-6, D=1e-14)
=============================================================================== Details =============================================================================== Layer builder for patankar package
All materials are represented as layers and be combined, merged with mathematical operations such as +. The general object general object is of class layer.
Specific materials with known properties have been derived: LDPE(),HDPE(),PP()…air()
List of implemnted materials:
| Class Name | Type | Material | Code |
|-------------------------|----------|---------------------------------|---------|
| AdhesiveAcrylate | adhesive | acrylate adhesive | Acryl |
| AdhesiveEVA | adhesive | EVA adhesive | EVA |
| AdhesiveNaturalRubber | adhesive | natural rubber adhesive | rubber |
| AdhesivePU | adhesive | polyurethane adhesive | PU |
| AdhesivePVAC | adhesive | PVAc adhesive | PVAc |
| AdhesiveSyntheticRubber | adhesive | synthetic rubber adhesive | sRubber |
| AdhesiveVAE | adhesive | VAE adhesive | VAE |
| Cardboard | paper | cardboard | board |
| HDPE | polymer | high-density polyethylene | HDPE |
| HIPS | polymer | high-impact polystyrene | HIPS |
| LDPE | polymer | low-density polyethylene | LDPE |
| LLDPE | polymer | linear low-density polyethylene | LLDPE |
| PA6 | polymer | polyamide 6 | PA6 |
| PA66 | polymer | polyamide 6,6 | PA6,6 |
| SBS | polymer | styrene-based polymer SBS | SBS |
| PBT | polymer | polybutylene terephthalate | PBT |
| PEN | polymer | polyethylene naphthalate | PEN |
| PP | polymer | isotactic polypropylene | PP |
| PPrubber | polymer | atactic polypropylene | aPP |
| PS | polymer | polystyrene | PS |
| Paper | paper | paper | paper |
| air | air | ideal gas | gas |
| gPET | polymer | glassy PET | PET |
| oPP | polymer | bioriented polypropylene | oPP |
| plasticizedPVC | polymer | plasticized PVC | pPVC |
| rPET | polymer | rubbery PET | rPET |
| rigidPVC | polymer | rigid PVC | PVC |
Mass transfer within each layer are governed by a diffusion coefficient, a Henri-like coefficient enabling to describe the partitioning between layers. All materials are automatically meshed using a modified finite volume technique exact at steady state and offering good accuracy in non-steady conditions.
A temperature and substance can be assigned to layers.
@version: 1.24 @project: SFPPy - SafeFoodPackaging Portal in Python initiative @author: INRAE\olivier.vitrac@agroparistech.fr @licence: MIT @Date: 2022-02-21 @rev. 2025-03-04
Expand source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
===============================================================================
SFPPy Module: Layer (Packaging Materials)
===============================================================================
Defines **packaging materials** as 1D layers. Supports:
- **Multilayer assembly (`layer1 + layer2`)**
- **Mass transfer modeling (`layer >> food`)**
- **Automatic meshing for finite-volume solvers**
**Main Components:**
- **Base Class: `layer`** (Defines all packaging materials)
- Properties: `D` (diffusivity), `k` (partition coefficient), `l` (thickness)
- Supports **+ (stacking)** and **splitting** layers
- Propagates contact temperature from `food.py`
- **Predefined Materials (Subclasses)**:
- `LDPE`, `PP`, `PET`, `Cardboard`, `Ink`
- **Dynamic Property Models:**
- `Dmodel()`, `kmodel()`: Call `property.py` to predict diffusion and partitioning
**Integration with SFPPy Modules:**
- Used in `migration.py` to define the **left-side boundary**.
- Retrieves chemical properties from `loadpubchem.py`.
- Works with `food.py` to model **food-contact** interactions.
Example:
```python
from patankar.layer import LDPE
A = LDPE(l=50e-6, D=1e-14)
```
===============================================================================
Details
===============================================================================
Layer builder for patankar package
All materials are represented as layers and be combined, merged with mathematical
operations such as +. The general object general object is of class layer.
Specific materials with known properties have been derived: LDPE(),HDPE(),PP()...air()
List of implemnted materials:
| Class Name | Type | Material | Code |
|-------------------------|----------|---------------------------------|---------|
| AdhesiveAcrylate | adhesive | acrylate adhesive | Acryl |
| AdhesiveEVA | adhesive | EVA adhesive | EVA |
| AdhesiveNaturalRubber | adhesive | natural rubber adhesive | rubber |
| AdhesivePU | adhesive | polyurethane adhesive | PU |
| AdhesivePVAC | adhesive | PVAc adhesive | PVAc |
| AdhesiveSyntheticRubber | adhesive | synthetic rubber adhesive | sRubber |
| AdhesiveVAE | adhesive | VAE adhesive | VAE |
| Cardboard | paper | cardboard | board |
| HDPE | polymer | high-density polyethylene | HDPE |
| HIPS | polymer | high-impact polystyrene | HIPS |
| LDPE | polymer | low-density polyethylene | LDPE |
| LLDPE | polymer | linear low-density polyethylene | LLDPE |
| PA6 | polymer | polyamide 6 | PA6 |
| PA66 | polymer | polyamide 6,6 | PA6,6 |
| SBS | polymer | styrene-based polymer SBS | SBS |
| PBT | polymer | polybutylene terephthalate | PBT |
| PEN | polymer | polyethylene naphthalate | PEN |
| PP | polymer | isotactic polypropylene | PP |
| PPrubber | polymer | atactic polypropylene | aPP |
| PS | polymer | polystyrene | PS |
| Paper | paper | paper | paper |
| air | air | ideal gas | gas |
| gPET | polymer | glassy PET | PET |
| oPP | polymer | bioriented polypropylene | oPP |
| plasticizedPVC | polymer | plasticized PVC | pPVC |
| rPET | polymer | rubbery PET | rPET |
| rigidPVC | polymer | rigid PVC | PVC |
Mass transfer within each layer are governed by a diffusion coefficient, a Henri-like coefficient
enabling to describe the partitioning between layers. All materials are automatically meshed using
a modified finite volume technique exact at steady state and offering good accuracy in non-steady
conditions.
A temperature and substance can be assigned to layers.
@version: 1.24
@project: SFPPy - SafeFoodPackaging Portal in Python initiative
@author: INRAE\\olivier.vitrac@agroparistech.fr
@licence: MIT
@Date: 2022-02-21
@rev. 2025-03-04
"""
# ---- History ----
# Created on Tue Jan 18 09:14:34 2022
# 2022-01-19 RC
# 2022-01-20 full indexing and simplification
# 2022-01-21 add split()
# 2022-01-22 add child classes for common polymers
# 2022-01-23 full implementation of units
# 2022-01-26 mesh() method generating mesh objects
# 2022-02-21 add compatibility with migration
oneline = "Build multilayer objects"
docstr = """
Build layer(s) for SENSPATANKAR
Example of caller:
from patankar.layer import layer
A=layer(D=1e-14,l=50e-6)
A
"""
# Package Dependencies
# ====================
# <-- generic packages -->
import sys
import inspect
import textwrap
import numpy as np
from copy import deepcopy as duplicate
# <-- local packages -->
if 'SIbase' not in dir(): # avoid loading it twice
from patankar.private.pint import UnitRegistry as SIbase
from patankar.private.pint import set_application_registry as fixSIbase
if 'migrant' not in dir():
from patankar.loadpubchem import migrant
__all__ = ['AdhesiveAcrylate', 'AdhesiveEVA', 'AdhesiveNaturalRubber', 'AdhesivePU', 'AdhesivePVAC', 'AdhesiveSyntheticRubber', 'AdhesiveVAE', 'Cardboard', 'HDPE', 'HIPS', 'LDPE', 'LLDPE', 'PA6', 'PA66', 'PBT', 'PEN', 'PP', 'PPrubber', 'PS', 'Paper', 'R', 'RT0K', 'SBS', 'SI', 'SIbase', 'T0K', 'air', 'check_units', 'fixSIbase', 'format_scientific_latex', 'gPET', 'help_layer', 'iRT0K', 'layer', 'layerLink', 'list_layer_subclasses', 'mesh', 'migrant', 'oPP', 'plasticizedPVC', 'qSI', 'rPET', 'rigidPVC', 'toSI']
__project__ = "SFPPy"
__author__ = "Olivier Vitrac"
__copyright__ = "Copyright 2022"
__credits__ = ["Olivier Vitrac"]
__license__ = "MIT"
__maintainer__ = "Olivier Vitrac"
__email__ = "olivier.vitrac@agroparistech.fr"
__version__ = "1.24"
# %% Private functions and classes
# Initialize unit conversion (intensive initialization with old Python versions)
# NB: degC and kelvin must be used for temperature
# conversion as obj,valueSI,unitSI = toSI(qSI(numvalue,"unit"))
# conversion as obj,valueSI,unitSI = toSI(qSI("value unit"))
def toSI(q): q=q.to_base_units(); return q,q.m,str(q.u)
NoUnits = 'a.u.' # value for arbitrary unit
UnknownUnits = 'N/A' # no non indentified units
if ("SI" not in locals()) or ("qSI" not in locals()):
SI = SIbase() # unit engine
fixSIbase(SI) # keep the same instance between calls
qSI = SI.Quantity # main unit consersion method from string
# constants (usable in layer object methods)
# define R,T0K,R*T0K,1/(R*T0K) with there SI units
constants = {}
R,constants["R"],constants["Runit"] = toSI(qSI(1,'avogadro_number*boltzmann_constant'))
T0K,constants["T0K"],constants["T0Kunit"] = toSI(qSI(0,'degC'))
RT0K,constants["RT0K"],constants["RT0Kunit"] = toSI(R*T0K)
iRT0K,constants["iRT0K"],constants["iRT0Kunit"] = toSI(1/RT0K)
# Concise data validator with unit convertor to SI
# To prevent many issues with temperature and to adhere to 2024 golden standard in layer
# defaulttempUnits has been set back to "degC" from "K".
def check_units(value,ProvidedUnits=None,ExpectedUnits=None,defaulttempUnits="degC"):
""" check numeric inputs and convert them to SI units """
# by convention, NumPy arrays and None are return unchanged (prevent nesting)
if isinstance(value,np.ndarray) or value is None:
return value,UnknownUnits
if isinstance(value,tuple):
if len(value) != 2:
raise ValueError('value should be a tuple: (value,"unit"')
ProvidedUnits = value[1]
value = value[0]
if isinstance(value,list): # the function is vectorized
value = np.array(value)
if {"degC", "K"} & {ProvidedUnits, ExpectedUnits}: # the value is a temperature
ExpectedUnits = defaulttempUnits if ExpectedUnits is None else ExpectedUnits
ProvidedUnits = ExpectedUnits if ProvidedUnits is None else ProvidedUnits
if ProvidedUnits=="degC" and ExpectedUnits=="K":
value += constants["T0K"]
elif ProvidedUnits=="K" and ExpectedUnits=="degC":
value -= constants["T0K"]
return np.array([value]),ExpectedUnits
else: # the value is not a temperature
ExpectedUnits = NoUnits if ExpectedUnits is None else ExpectedUnits
if (ProvidedUnits==ExpectedUnits) or (ProvidedUnits==NoUnits) or (ExpectedUnits==None):
conversion =1 # no conversion needed
units = ExpectedUnits if ExpectedUnits is not None else NoUnits
else:
q0,conversion,units = toSI(qSI(1,ProvidedUnits))
return np.array([value*conversion]),units
# _toSI: function helper for the enduser outside layer
def _toSI(value=None):
'''return an SI value from (value,"unit")'''
if not isinstance(value,tuple) or len(value)!=2 \
or not isinstance(value[0],(float,int,list,np.ndarray)) \
or not isinstance(value[1],str):
raise ValueError('value must be (currentvalue,"unit") - for example: (10,"days")')
return check_units(value)[0]
# formatsci equivalent
def format_scientific_latex(value, numdigits=4, units=None, prefix="",mathmode="$"):
"""
Formats a number in scientific notation only when necessary, using LaTeX.
Parameters:
-----------
value : float
The number to format.
numdigits : int, optional (default=4)
Number of significant digits for formatting.
units : str, optional (default=None)
LaTeX representation of units. If None, no units are added.
prefix: str, optional (default="")
mathmode: str, optional (default="$")
Returns:
--------
str
The formatted number in standard or LaTeX scientific notation.
Examples:
---------
>>> format_scientific_latex(1e-12)
'$10^{-12}$'
>>> format_scientific_latex(1.5e-3)
'0.0015'
>>> format_scientific_latex(1.3e10)
'$1.3 \\cdot 10^{10}$'
>>> format_scientific_latex(0.00341)
'0.00341'
>>> format_scientific_latex(3.41e-6)
'$3.41 \\cdot 10^{-6}$'
"""
if value == 0:
return "$0$" if units is None else rf"$0 \, {units}$"
# Get formatted number using Matlab-like %g behavior
formatted = f"{value:.{numdigits}g}"
# If the formatting results in an `e` notation, convert to LaTeX
if "e" in formatted or "E" in formatted:
coefficient, exponent = formatted.split("e")
exponent = int(exponent) # Convert exponent to integer
# Remove trailing zeros in coefficient
coefficient = coefficient.rstrip("0").rstrip(".") # Ensures "1.00" -> "1"
# LaTeX scientific format
sci_notation = rf"{prefix}{coefficient} \cdot 10^{{{exponent}}}"
return sci_notation if units is None else rf"{mathmode}{sci_notation} \, {units}{mathmode}"
# Otherwise, return standard notation
return formatted if units is None else rf"{mathmode}{prefix}{formatted} \, {units}{mathmode}"
# helper function to list all classes
def list_layer_subclasses():
"""
Lists all classes in this module that derive from 'layer',
along with their layertype and layermaterial properties.
Returns:
list of tuples (classname, layertype, layermaterial)
"""
subclasses_info = []
current_module = sys.modules[__name__] # This refers to layer.py itself
for name, obj in inspect.getmembers(current_module, inspect.isclass):
# Make sure 'obj' is actually a subclass of layer (and not 'layer' itself)
if obj is not layer and issubclass(obj, layer):
try:
# Instantiate with default parameters so that .layertype / .layermaterial are accessible
instance = obj()
subclasses_info.append(
{"classname":name,
"type":instance._type[0],
"material":instance._material[0],
"code":instance._code[0]}
)
except TypeError as e:
# Log error and rethrow for debugging
print(f"⚠️ Error: Could not instantiate class '{name}'. Check its constructor.")
print(f"🔍 Exception: {e}")
raise # Rethrow the error with full traceback
return subclasses_info
# general help for layer
def help_layer():
"""
Print all subclasses with their type/material info in a Markdown table with dynamic column widths.
"""
derived = list_layer_subclasses()
# Extract table content
headers = ["Class Name", "Type", "Material", "Code"]
rows = [[item["classname"], item["type"], item["material"], item["code"]] for item in derived]
# Compute column widths based on content
col_widths = [max(len(str(cell)) for cell in col) for col in zip(headers, *rows)]
# Formatting row template
row_format = "| " + " | ".join(f"{{:<{w}}}" for w in col_widths) + " |"
# Print header
print(row_format.format(*headers))
print("|-" + "-|-".join("-" * w for w in col_widths) + "-|")
# Print table rows
for row in rows:
print(row_format.format(*row))
# generic class to store linked parameter values in a sparse manner
class layerLink:
"""
A sparse representation of properties (`D`, `k`, `C0`) used in `layer` instances.
This class allows storing and manipulating selected values of a property (`D`, `k`, or `C0`)
while keeping a sparse structure. It enables seamless interaction with `layer` objects
by overriding values dynamically and ensuring efficient memory usage.
The primary use case is to fit and control property values externally while keeping
the `layer` representation internally consistent.
Attributes
----------
property : str
The name of the property linked (`"D"`, `"k"`, or `"C0"`).
indices : np.ndarray
A NumPy array storing the indices of explicitly defined values.
values : np.ndarray
A NumPy array storing the corresponding values at `indices`.
length : int
The total length of the sparse vector, ensuring coverage of all indices.
replacement : str, optional
Defines how missing values are handled:
- `"repeat"`: Propagates the last known value beyond `length`.
- `"periodic"`: Cycles through known values beyond `length`.
- Default: No automatic replacement within `length`.
Methods
-------
set(index, value)
Sets values at specific indices. If `None` or `np.nan` is provided, the index is removed.
get(index=None)
Retrieves values at the given indices. Returns `NaN` for missing values.
getandreplace(indices, altvalues)
Similar to `get()`, but replaces `NaN` values with corresponding values from `altvalues`.
getfull(altvalues)
Returns the full vector using `getandreplace(None, altvalues)`.
lengthextension()
Ensures `length` covers all stored indices (`max(indices) + 1`).
rename(new_property_name)
Renames the `property` associated with this `layerLink`.
nzcount()
Returns the number of explicitly stored (nonzero) elements.
__getitem__(index)
Allows retrieval using `D_link[index]`, equivalent to `get(index)`.
__setitem__(index, value)
Allows assignment using `D_link[index] = value`, equivalent to `set(index, value)`.
__add__(other)
Concatenates two `layerLink` instances with the same property.
__mul__(n)
Repeats the `layerLink` instance `n` times, shifting indices accordingly.
Examples
--------
Create a `layerLink` for `D` and manipulate its values:
```python
D_link = layerLink("D")
D_link.set([0, 2], [1e-14, 3e-14])
print(D_link.get()) # Expected: array([1e-14, nan, 3e-14])
D_link[1] = 2e-14
print(D_link.get()) # Expected: array([1e-14, 2e-14, 3e-14])
```
Concatenating two `layerLink` instances:
```python
A = layerLink("D")
A.set([0, 2], [1e-14, 3e-14])
B = layerLink("D")
B.set([1, 3], [2e-14, 4e-14])
C = A + B # Concatenates while shifting indices
print(C.get()) # Expected: array([1e-14, 3e-14, nan, nan, 2e-14, 4e-14])
```
Handling missing values with `getandreplace()`:
```python
alt_values = np.array([5e-14, 6e-14, 7e-14, 8e-14])
print(D_link.getandreplace([0, 1, 2, 3], alt_values))
# Expected: array([1e-14, 2e-14, 3e-14, 8e-14]) # Fills NaNs from alt_values
```
Ensuring correct behavior for `*`:
```python
B = A * 3 # Repeats A three times
print(B.indices) # Expected: [0, 2, 4, 6, 8, 10]
print(B.values) # Expected: [1e-14, 3e-14, 1e-14, 3e-14, 1e-14, 3e-14]
print(B.length) # Expected: 3 * A.length
```
Other Examples:
----------------
### **Creating a Link**
D_link = layerLink("D", indices=[1, 3], values=[5e-14, 7e-14], length=4)
print(D_link) # <Link for D: 2 of 4 replacement values>
### **Retrieving Values**
print(D_link.get()) # Full vector with None in unspecified indices
print(D_link.get(1)) # Returns 5e-14
print(D_link.get([0,2])) # Returns [None, None]
### **Setting Values**
D_link.set(2, 6e-14)
print(D_link.get()) # Now index 2 is replaced
### **Resetting with a Prototype**
prototype = [None, 5e-14, None, 7e-14, 8e-14]
D_link.reset(prototype)
print(D_link.get()) # Now follows the new structure
### **Getting and Setting Values with []**
D_link = layerLink("D", indices=[1, 3, 5], values=[5e-14, 7e-14, 6e-14], length=10)
print(D_link[3]) # ✅ Returns 7e-14
print(D_link[:5]) # ✅ Returns first 5 elements (with NaNs where undefined)
print(D_link[[1, 3]]) # ✅ Returns [5e-14, 7e-14]
D_link[2] = 9e-14 # ✅ Sets D[2] to 9e-14
D_link[0:4:2] = [1e-14, 2e-14] # ✅ Sets D[0] = 1e-14, D[2] = 2e-14
print(len(D_link)) # ✅ Returns 10 (full vector length)
###**Practical Syntaxes**
D_link = layerLink("D")
D_link[2] = 3e-14 # ✅ single value
D_link[0] = 1e-14
print(D_link.get())
print(D_link[1])
print(repr(D_link))
D_link[:4] = 1e-16 # ✅ Fills indices 0,1,2,3 with 1e-16
print(D_link.get()) # ✅ Outputs: [1e-16, 1e-16, 1e-16, 1e-16, nan, 1e-14]
D_link[[1,2]] = None # ✅ Fills indices 0,1,2,3 with 1e-16
print(D_link.get()) # ✅ Outputs: [1e-16, 1e-16, 1e-16, 1e-16, nan, 1e-14]
D_link[[0]] = 1e-10
print(D_link.get())
###**How it works inside layer: a short simulation**
# layerLink created by user
duser = layerLink()
duser.getfull([1e-15,2e-15,3e-15])
duser[0] = 1e-10
duser.getfull([1e-15,2e-15,3e-15])
duser[1]=1e-9
duser.getfull([1e-15,2e-15,3e-15])
# layerLink used internally
dalias=duser
dalias[1]=2e-11
duser.getfull([1e-15,2e-15,3e-15,4e-15])
dalias[1]=2.1e-11
duser.getfull([1e-15,2e-15,3e-15,4e-15])
###**Combining layerLinks instances**
A = layerLink("D")
A.set([0, 2], [1e-11, 3e-11]) # length=3
B = layerLink("D")
B.set([1, 3], [2e-14, 4e-12]) # length=4
C = A + B
print(C.indices) # Expected: [0, 2, 4, 6]
print(C.values) # Expected: [1.e-11 3.e-11 2.e-14 4.e-12]
print(C.length) # Expected: 3 + 4 = 7
TEST CASES:
-----------
print("🔹 Test 1: Initialize empty layerLink")
D_link = layerLink("D")
print(D_link.get()) # Expected: array([]) or array([nan, nan, nan]) if length is pre-set
print(repr(D_link)) # Expected: No indices set
print("\n🔹 Test 2: Assigning values at specific indices")
D_link[2] = 3e-14
D_link[0] = 1e-14
print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14])
print(D_link[1]) # Expected: nan
print("\n🔹 Test 3: Assign multiple values at once")
D_link[[1, 4]] = [2e-14, 5e-14]
print(D_link.get()) # Expected: array([1.e-14, 2.e-14, 3.e-14, nan, 5.e-14])
print("\n🔹 Test 4: Remove a single index")
D_link[1] = None
print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14, nan, 5.e-14])
print("\n🔹 Test 5: Remove multiple indices at once")
D_link[[0, 2]] = None
print(D_link.get()) # Expected: array([nan, nan, nan, nan, 5.e-14])
print("\n🔹 Test 6: Removing indices using a slice")
D_link[3:5] = None
print(D_link.get()) # Expected: array([nan, nan, nan, nan, nan])
print("\n🔹 Test 7: Assign new values after removals")
D_link[1] = 7e-14
D_link[3] = 8e-14
print(D_link.get()) # Expected: array([nan, 7.e-14, nan, 8.e-14, nan])
print("\n🔹 Test 8: Check periodic replacement")
D_link = layerLink("D", replacement="periodic")
D_link[2] = 3e-14
D_link[0] = 1e-14
print(D_link[5]) # Expected: 1e-14 (since 5 mod 2 = 0)
print("\n🔹 Test 9: Check repeat replacement")
D_link = layerLink("D", replacement="repeat")
D_link[2] = 3e-14
D_link[0] = 1e-14
print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14])
print(D_link[3]) # Expected: 3e-14 (repeat last known value)
print("\n🔹 Test 10: Resetting with a prototype")
D_link.reset([None, 5e-14, None, 7e-14])
print(D_link.get()) # Expected: array([nan, 5.e-14, nan, 7.e-14])
print("\n🔹 Test 11: Edge case - Assigning nan explicitly")
D_link[1] = np.nan
print(D_link.get()) # Expected: array([nan, nan, nan, 7.e-14])
print("\n🔹 Test 12: Assigning a range with a scalar value (broadcasting)")
D_link[0:3] = 9e-14
print(D_link.get()) # Expected: array([9.e-14, 9.e-14, 9.e-14, 7.e-14])
print("\n🔹 Test 13: Assigning a slice with a list of values")
D_link[1:4] = [6e-14, 5e-14, 4e-14]
print(D_link.get()) # Expected: array([9.e-14, 6.e-14, 5.e-14, 4.e-14])
print("\n🔹 Test 14: Length updates correctly after removals")
D_link[[1, 2]] = None
print(len(D_link)) # Expected: 4 (since max index is 3)
print("\n🔹 Test 15: Setting index beyond length auto-extends")
D_link[6] = 2e-14
print(len(D_link)) # Expected: 7 (since max index is 6)
print(D_link.get()) # Expected: array([9.e-14, nan, nan, 4.e-14, nan, nan, 2.e-14])
"""
def __init__(self, property="D", indices=None, values=None, length=None,
replacement="repeat", dtype=np.float64, maxlength=None):
"""constructs a link"""
self.property = property # "D", "k", or "C0"
self.replacement = replacement
self.dtype = dtype
self._maxlength = maxlength
if isinstance(indices,(int,float)): indices = [indices]
if isinstance(values,(int,float)): values = [values]
if indices is None or values is None:
self.indices = np.array([], dtype=int)
self.values = np.array([], dtype=dtype)
else:
self.indices = np.array(indices, dtype=int)
self.values = np.array(values, dtype=dtype)
self.length = length if length is not None else (self.indices.max() + 1 if self.indices.size > 0 else 0)
self._validate()
def _validate(self):
"""Ensures consistency between indices and values."""
if len(self.indices) != len(self.values):
raise ValueError("indices and values must have the same length.")
if self.indices.size > 0 and self.length < self.indices.max() + 1:
raise ValueError("length must be at least max(indices) + 1.")
def reset(self, prototypevalues):
"""
Resets the link instance based on the prototype values.
- Stores only non-None values.
- Updates `indices`, `values`, and `length` accordingly.
"""
self.indices = np.array([i for i, v in enumerate(prototypevalues) if v is not None], dtype=int)
self.values = np.array([v for v in prototypevalues if v is not None], dtype=self.dtype)
self.length = len(prototypevalues) # Update the total length
def get(self, index=None):
"""
Retrieves values based on index or returns the full vector.
Rules:
- If `index=None`, returns the full vector with overridden values (no replacement applied).
- If `index` is a scalar, returns the corresponding value, applying replacement rules if needed.
- If `index` is an array, returns an array of the requested indices, applying replacement rules.
Returns:
- NumPy array with requested values.
"""
if index is None:
# Return the full vector WITHOUT applying any replacement
full_vector = np.full(self.length, np.nan, dtype=self.dtype)
full_vector[self.indices] = self.values # Set known values
return full_vector
if np.isscalar(index):
return self._get_single(index)
# Ensure index is an array
index = np.array(index, dtype=int)
return np.array([self._get_single(i) for i in index], dtype=self.dtype)
def _get_single(self, i):
"""Retrieves the value for a single index, applying rules if necessary."""
if i in self.indices:
return self.values[np.where(self.indices == i)[0][0]]
if i >= self.length: # Apply replacement *only* for indices beyond length
if self.replacement == "periodic":
return self.values[i % len(self.values)]
elif self.replacement == "repeat":
return self._get_single(self.length - 1) # Repeat last known value
return np.nan # Default case for undefined in-bounds indices
def set(self, index, value):
"""
Sets values at specific indices.
- If `index=None`, resets the link with `value`.
- If `index` is a scalar, updates or inserts the value.
- If `index` is an array, updates corresponding values.
- If `value` is `None` or `np.nan`, removes the corresponding index.
"""
if index is None:
self.reset(value)
return
index = np.array(index, dtype=int)
value = np.array(value, dtype=self.dtype)
# check against _maxlength if defined
if self._maxlength is not None:
if np.any(index>=self._maxlength):
raise IndexError(f"index cannot exceeds the number of layers-1 {self._maxlength-1}")
# Handle scalars properly
if np.isscalar(index):
index = np.array([index])
value = np.array([value])
# Detect None or NaN values and remove those indices
mask = np.isnan(value) if value.dtype.kind == 'f' else np.array([v is None for v in value])
if np.any(mask):
self._remove_indices(index[mask]) # Remove these indices
index, value = index[~mask], value[~mask] # Keep only valid values
if index.size > 0: # If there are remaining valid values, store them
for i, v in zip(index, value):
if i in self.indices:
self.values[np.where(self.indices == i)[0][0]] = v
else:
self.indices = np.append(self.indices, i)
self.values = np.append(self.values, v)
# Update length to ensure it remains valid
if self.indices.size > 0:
self.length = max(self.indices) + 1 # Adjust length based on max index
else:
self.length = 0 # Reset to 0 if empty
self._validate()
def _remove_indices(self, indices):
"""
Removes indices from `self.indices` and `self.values` and updates length.
"""
mask = np.isin(self.indices, indices, invert=True)
self.indices = self.indices[mask]
self.values = self.values[mask]
# Update length after removal
if self.indices.size > 0:
self.length = max(self.indices) + 1 # Adjust length based on remaining max index
else:
self.length = 0 # Reset to 0 if no indices remain
def reshape(self, new_length):
"""
Reshapes the link instance to a new length.
- If indices exceed new_length-1, they are removed with a warning.
- If replacement operates beyond new_length-1, a warning is issued.
"""
if new_length < self.length:
invalid_indices = self.indices[self.indices >= new_length]
if invalid_indices.size > 0:
print(f"⚠️ Warning: Indices {invalid_indices.tolist()} are outside new length {new_length}. They will be removed.")
mask = self.indices < new_length
self.indices = self.indices[mask]
self.values = self.values[mask]
# Check if replacement would be applied beyond the new length
if self.replacement == "repeat" and self.indices.size > 0 and self.length > new_length:
print(f"⚠️ Warning: Repeat rule was defined for indices beyond {new_length-1}, but will not be used.")
if self.replacement == "periodic" and self.indices.size > 0 and self.length > new_length:
print(f"⚠️ Warning: Periodic rule was defined for indices beyond {new_length-1}, but will not be used.")
self.length = new_length
def __repr__(self):
"""Returns a detailed string representation."""
txt = (f"Link(property='{self.property}', indices={self.indices.tolist()}, "
f"values={self.values.tolist()}, length={self.length}, replacement='{self.replacement}')")
print(txt)
return(str(self))
def __str__(self):
"""Returns a compact summary string."""
return f"<{self.property}:{self.__class__.__name__}: {len(self.indices)}/{self.length} values>"
# Override `len()`
def __len__(self):
"""Returns the length of the vector managed by the link object."""
return self.length
# Override `getitem` (support for indexing and slicing)
def __getitem__(self, index):
"""
Allows `D_link[index]` or `D_link[slice]` to retrieve values.
- If `index` is an integer, returns a single value.
- If `index` is a slice or list/array, returns a NumPy array of values.
"""
if isinstance(index, slice):
return self.get(np.arange(index.start or 0, index.stop or self.length, index.step or 1))
return self.get(index)
# Override `setitem` (support for indexing and slicing)
def __setitem__(self, index, value):
"""
Allows `D_link[index] = value` or `D_link[slice] = list/scalar`.
- If `index` is an integer, updates or inserts a single value.
- If `index` is a slice or list/array, updates multiple values.
- If `value` is `None` or `np.nan`, removes the corresponding index.
"""
if isinstance(index, slice):
indices = np.arange(index.start or 0, index.stop or self.length, index.step or 1)
elif isinstance(index, (list, np.ndarray)): # Handle non-contiguous indices
indices = np.array(index, dtype=int)
elif np.isscalar(index): # Single index assignment
indices = np.array([index], dtype=int)
else:
raise TypeError(f"Unsupported index type: {type(index)}")
if value is None or (isinstance(value, float) and np.isnan(value)): # Remove these indices
self._remove_indices(indices)
else:
values = np.full_like(indices, value, dtype=self.dtype) if np.isscalar(value) else np.array(value, dtype=self.dtype)
if len(indices) != len(values):
raise ValueError(f"Cannot assign {len(values)} values to {len(indices)} indices.")
self.set(indices, values)
def getandreplace(self, indices=None, altvalues=None):
"""
Retrieves values for the given indices, replacing NaN values with corresponding values from altvalues.
- If `indices` is None or empty, it defaults to `[0, 1, ..., self.length - 1]`
- altvalues should be a NumPy array with the same dtype as self.values.
- altvalues **can be longer than** self.length, but **cannot be shorter than the highest requested index**.
- If an index is undefined (`NaN` in get()), it is replaced with altvalues[index].
Parameters:
----------
indices : list or np.ndarray (default: None)
The indices to retrieve values for. If None, defaults to full range `[0, ..., self.length - 1]`.
altvalues : list or np.ndarray
Alternative values to use where `get()` returns `NaN`.
Returns:
-------
np.ndarray
A NumPy array of values, with NaNs replaced by altvalues.
"""
if indices is None or len(indices) == 0:
indices = np.arange(self.length) # Default to full range
indices = np.array(indices, dtype=int)
altvalues = np.array(altvalues, dtype=self.dtype)
max_requested_index = indices.max() if indices.size > 0 else 0
if max_requested_index >= altvalues.shape[0]: # Ensure altvalues covers all requested indices
raise ValueError(
f"altvalues is too short! It has length {altvalues.shape[0]}, but requested index {max_requested_index}."
)
# Get original values
original_values = self.get(indices)
# Replace NaN values with corresponding values from altvalues
mask_nan = np.isnan(original_values)
original_values[mask_nan] = altvalues[indices[mask_nan]]
return original_values
def getfull(self, altvalues):
"""
Retrieves the full vector using `getandreplace(None, altvalues)`.
- If `length == 0`, returns `altvalues` as a NumPy array of the correct dtype.
- Extends `self.length` to match `altvalues` if it's shorter.
- Supports multidimensional `altvalues` by flattening it.
Parameters:
----------
altvalues : list or np.ndarray
Alternative values to use where `get()` returns `NaN`.
Returns:
-------
np.ndarray
Full vector with NaNs replaced by altvalues.
"""
# Convert altvalues to a NumPy array and flatten if needed
altvalues = np.array(altvalues, dtype=self.dtype).flatten()
# If self has no length, return altvalues directly
if self.length == 0:
return altvalues
# Extend self.length to match altvalues if needed
if self.length < altvalues.shape[0]:
self.length = altvalues.shape[0]
return self.getandreplace(None, altvalues)
@property
def nzlength(self):
"""
Returns the number of stored nonzero elements (i.e., indices with values).
"""
return len(self.indices)
def lengthextension(self):
"""
Ensures that the length of the layerLink instance is at least `max(indices) + 1`.
- If there are no indices, the length remains unchanged.
- If `length` is already sufficient, nothing happens.
- Otherwise, it extends `length` to `max(indices) + 1`.
"""
if self.indices.size > 0: # Only extend if there are indices
self.length = max(self.length, max(self.indices) + 1)
def rename(self, new_property_name):
"""
Renames the property associated with this link.
Parameters:
----------
new_property_name : str
The new property name.
Raises:
-------
TypeError:
If `new_property_name` is not a string.
"""
if not isinstance(new_property_name, str):
raise TypeError(f"Property name must be a string, got {type(new_property_name).__name__}.")
self.property = new_property_name
def __add__(self, other):
"""
Concatenates two layerLink instances.
- Only allowed if both instances have the same property.
- Calls `lengthextension()` on both instances before summing lengths.
- Shifts `other`'s indices by `self.length` to maintain sparsity.
- Concatenates values and indices.
Returns:
-------
layerLink
A new concatenated layerLink instance.
"""
if not isinstance(other, layerLink):
raise TypeError(f"Cannot concatenate {type(self).__name__} with {type(other).__name__}")
if self.property != other.property:
raise ValueError(f"Cannot concatenate: properties do not match ('{self.property}' vs. '{other.property}')")
# Ensure lengths are properly extended before computing new length
self.lengthextension()
other.lengthextension()
# Create a new instance for the result
result = layerLink(self.property)
# Copy self's values
result.indices = np.array(self.indices, dtype=int)
result.values = np.array(self.values, dtype=self.dtype)
# Adjust other’s indices and add them
shifted_other_indices = np.array(other.indices) + self.length
result.indices = np.concatenate([result.indices, shifted_other_indices])
result.values = np.concatenate([result.values, np.array(other.values, dtype=self.dtype)])
# ✅ Correct length calculation: Sum of the two lengths (assuming lengths are extended)
result.length = self.length + other.length
return result
def __mul__(self, n):
"""
Repeats the layerLink instance `n` times.
- Uses `+` to concatenate multiple copies with shifted indices.
- Each repetition gets indices shifted by `self.length * i`.
Returns:
-------
layerLink
A new layerLink instance with repeated data.
"""
if not isinstance(n, int) or n <= 0:
raise ValueError("Multiplication factor must be a positive integer")
result = layerLink(self.property)
for i in range(n):
shifted_instance = layerLink(self.property)
shifted_instance.indices = np.array(self.indices) + i * self.length
shifted_instance.values = np.array(self.values, dtype=self.dtype)
shifted_instance.length = self.length
result += shifted_instance # Use `+` to merge each repetition
return result
# %% Core class: layer
# default values (usable in layer object methods)
# these default values can be moved in a configuration file
# Main class definition
# =======================
class layer:
"""
------------------------------------------------------------------------------
**Core Functionality**
------------------------------------------------------------------------------
This class models layers in food packaging, handling mass transfer, partitioning,
and meshing for finite-volume simulations using a modified Patankar method.
Layers can be assembled into multilayers via the `+` operator and support
dynamic property linkage using `layerLink`.
------------------------------------------------------------------------------
**Key Properties**
------------------------------------------------------------------------------
- `l`: Thickness of the layer (m)
- `D`: Diffusion coefficient (m²/s)
- `k`: Partition coefficient (dimensionless)
- `C0`: Initial concentration (arbitrary units)
- `rho`: Density (kg/m³)
- `T`: Contact temperature (°C)
- `substance`: Migrant/substance modeled for diffusion
- `medium`: The food medium in contact with the layer
- `Dmodel`, `kmodel`: Callable models for diffusion and partitioning
------------------------------------------------------------------------------
**Methods**
------------------------------------------------------------------------------
- `__add__(self, other)`: Combines two layers into a multilayer structure.
- `__mul__(self, n)`: Duplicates a layer `n` times to create a multilayer.
- `__getitem__(self, i)`: Retrieves a sublayer from a multilayer.
- `__setitem__(self, i, other)`: Replaces sublayers in a multilayer structure.
- `mesh(self)`: Generates a numerical mesh for finite-volume simulations.
- `struct(self)`: Returns a dictionary representation of the layer properties.
- `resolvename(param_value, param_key, **unresolved)`: Resolves synonyms for parameter names.
- `help(cls)`: Displays a dynamically formatted summary of input parameters.
------------------------------------------------------------------------------
**Integration with SFPPy Modules**
------------------------------------------------------------------------------
- Works with `migration.py` for mass transfer simulations.
- Interfaces with `food.py` to define food-contact conditions.
- Uses `property.py` for predicting diffusion (`D`) and partitioning (`k`).
- Connects with `geometry.py` for 3D packaging simulations.
------------------------------------------------------------------------------
**Usage Example**
------------------------------------------------------------------------------
```python
from patankar.layer import LDPE, PP, layerLink
# Define a polymer layer with default properties
A = LDPE(l=50e-6, D=1e-14)
# Create a multilayer structure
B = PP(l=200e-6, D=1e-15)
multilayer = A + B
# Assign dynamic property linkage
k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer
multilayer.klink = k_link
# Simulate migration
from patankar.migration import senspatankar
from patankar.food import ethanol
medium = ethanol()
solution = senspatankar(multilayer, medium)
solution.plotCF()
```
------------------------------------------------------------------------------
**Notes**
------------------------------------------------------------------------------
- This class supports dynamic property inheritance, meaning `D` and `k` can be computed
based on the substance defined in `substance` and `medium`.
- The `layerLink` mechanism allows parameter adjustments without modifying the core object.
- The modified finite-volume meshing ensures **accurate steady-state and transient** behavior.
"""
# -----------------------------------------------------------------------------
# Class attributes that can be overidden in instances.
# Their default values are set in classes and overriden with similar
# instance properties with @property.setter.
# These values cannot be set during construction, but only after instantiation.
# -----------------------------------------------------------------------------
# These properties are essential for model predictions, they cannot be customized
# beyond the rules accepted by the model predictors (they are not metadata)
_physicalstate = "solid" # solid (default), liquid, gas, porous
_chemicalclass = "polymer" # polymer (default), other
_chemicalsubstance = None # None (default), monomer for polymers
_polarityindex = 0.0 # polarity index (roughly: 0=hexane, 10=water)
# Low-level prediction properties (these properties are common with patankar.food)
_lowLevelPredictionPropertyList = ["physicalstate","chemicalclass",
"chemicalsubstance","polarityindex","ispolymer","issolid"]
# --------------------------------------------------------------------
# PRIVATE PROPERTIES (cannot be changed by the user)
# __ read only attributes
# _ private attributes (not public)
# --------------------------------------------------------------------
__description = "LAYER object" # description
__version = 1.0 # version
__contact = "olivier.vitrac@agroparistech.fr" # contact person
_printformat = "%0.4g" # format to display D, k, l values
# Synonyms dictionary: Maps alternative names to the actual parameter
# these synonyms can be used during construction
_synonyms = {
"substance": {"migrant", "compound", "chemical","molecule","solute"},
"medium": {"food","simulant","fluid","liquid","contactmedium"},
"C0": {"CP0", "Cp0"},
"l": {"lp", "lP"},
"D": {"Dp", "DP"},
"k": {"kp", "kP"},
"T": {"temp","Temp","temperature","Temperature",
"contacttemperature","ContactTemperature","contactTemperature"}
}
# Default values for parameters (note that Td cannot be changed by the end-user)
_defaults = {
"l": 5e-5, # Thickness (m)
"D": 1e-14, # Diffusion coefficient (m^2/s)
"k": 1.0, # Henri-like coefficient (dimensionless)
"C0": 1000, # Initial concentration (arbitrary units)
"rho": 1000, # Default density (kg/m³)
"T": 40.0, # Default temperature (°C)
"Td": 25.0, # Reference temperature for densities (°C)
# Units (do not change)
"lunit": "m",
"Dunit": "m**2/s",
"kunit": "a.u.", # NoUnits
"Cunit": "a.u.", # NoUnits
"rhounit": "kg/m**3",
"Tunit": "degC", # Temperatures are indicated in °C instead of K (to reduce end-user mistakes)
# Layer properties
"layername": "my layer",
"layertype": "unknown type",
"layermaterial": "unknown material",
"layercode": "N/A",
# Mesh parameters
"nmeshmin": 20,
"nmesh": 600,
# Substance
"substance": None,
"simulant": None,
# Other parameters
"verbose": None,
"verbosity": 2
}
# List units
_parametersWithUnits = {
"l": "m",
"D": "m**2/s",
"k": "a.u.",
"C": "a.u.",
"rhp": "kg/m**3",
"T": "degC",
}
# Brief descriptions for each parameter
_descriptionInputs = {
"l": "Thickness of the layer (m)",
"D": "Diffusion coefficient (m²/s)",
"k": "Henri-like coefficient (dimensionless)",
"C0": "Initial concentration (arbitrary units)",
"rho": "Density of the material (kg/m³)",
"T": "Layer temperature (°C)",
"Td": "Reference temperature for densities (°C)",
"lunit": "Unit of thickness (default: m)",
"Dunit": "Unit of diffusion coefficient (default: m²/s)",
"kunit": "Unit of Henri-like coefficient (default: a.u.)",
"Cunit": "Unit of initial concentration (default: a.u.)",
"rhounit": "Unit of density (default: kg/m³)",
"Tunit": "Unit of temperature (default: degC)",
"layername": "Name of the layer",
"layertype": "Type of layer (e.g., polymer, ink, air)",
"layermaterial": "Material composition of the layer",
"layercode": "Identification code for the layer",
"nmeshmin": "Minimum number of FV mesh elements for the layer",
"nmesh": "Number of FV mesh elements for numerical computation",
"verbose": "Verbose mode (None or boolean)",
"verbosity": "Level of verbosity for debug messages (integer)"
}
# --------------------------------------------------------------------
# CONSTRUCTOR OF INSTANCE PROPERTIES
# None = missing numeric value (managed by default)
# --------------------------------------------------------------------
def __init__(self,
l=None, D=None, k=None, C0=None, rho=None, T=None,
lunit=None, Dunit=None, kunit=None, Cunit=None, rhounit=None, Tunit=None,
layername=None,layertype=None,layermaterial=None,layercode=None,
substance = None, medium = None,
# Dmodel = None, kmodel = None, they are defined via migrant (future overrides)
nmesh=None, nmeshmin=None, # simulation parametes
# link properties (for fitting and linking properties across simulations)
Dlink=None, klink=None, C0link=None, Tlink=None, llink=None,
verbose=None, verbosity=2,**unresolved):
"""
Parameters
----------
layername : TYPE, optional, string
DESCRIPTION. Layer Name. The default is "my layer".
layertype : TYPE, optional, string
DESCRIPTION. Layer Type. The default is "unknown type".
layermaterial : TYPE, optional, string
DESCRIPTION. Material identification . The default is "unknown material".
PHYSICAL QUANTITIES
l : TYPE, optional, scalar or tupple (value,"unit")
DESCRIPTION. Thickness. The default is 50e-6 (m).
D : TYPE, optional, scalar or tupple (value,"unit")
DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s).
k : TYPE, optional, scalar or tupple (value,"unit")
DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.).
C0 : TYPE, optional, scalar or tupple (value,"unit")
DESCRIPTION. Initial concentration. The default is 1000 (a.u.).
PHYSICAL UNITS
lunit : TYPE, optional, string
DESCRIPTION. Length units. The default unit is "m.
Dunit : TYPE, optional, string
DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s".
kunit : TYPE, optional, string
DESCRIPTION. Henry-like coefficient. The default unit is "a.u.".
Cunit : TYPE, optional, string
DESCRIPTION. Initial concentration. The default unit is "a.u.".
Returns
-------
a monolayer object which can be assembled into a multilayer structure
"""
# resolve alternative names used by end-users
substance = layer.resolvename(substance,"substance",**unresolved)
medium = layer.resolvename(medium, "medium", **unresolved)
C0 = layer.resolvename(C0,"C0",**unresolved)
l = layer.resolvename(l,"l",**unresolved)
D = layer.resolvename(D,"D",**unresolved)
k = layer.resolvename(k,"k",**unresolved)
T = layer.resolvename(T,"T",**unresolved)
# Assign defaults only if values are not provided
l = l if l is not None else layer._defaults["l"]
D = D if D is not None else layer._defaults["D"]
k = k if k is not None else layer._defaults["k"]
C0 = C0 if C0 is not None else layer._defaults["C0"]
rho = rho if rho is not None else layer._defaults["rho"]
T = T if T is not None else layer._defaults["T"]
lunit = lunit if lunit is not None else layer._defaults["lunit"]
Dunit = Dunit if Dunit is not None else layer._defaults["Dunit"]
kunit = kunit if kunit is not None else layer._defaults["kunit"]
Cunit = Cunit if Cunit is not None else layer._defaults["Cunit"]
rhounit = rhounit if rhounit is not None else layer._defaults["rhounit"]
Tunit = Tunit if Tunit is not None else layer._defaults["Tunit"]
nmesh = nmesh if nmesh is not None else layer._defaults["nmesh"]
nmeshmin = nmeshmin if nmeshmin is not None else layer._defaults["nmeshmin"]
verbose = verbose if verbose is not None else layer._defaults["verbose"]
verbosity = verbosity if verbosity is not None else layer._defaults["verbosity"]
# Assign layer id properties
layername = layername if layername is not None else layer._defaults["layername"]
layertype = layertype if layertype is not None else layer._defaults["layertype"]
layermaterial = layermaterial if layermaterial is not None else layer._defaults["layermaterial"]
layercode = layercode if layercode is not None else layer._defaults["layercode"]
# validate all physical paramaters with their units
l,lunit = check_units(l,lunit,layer._defaults["lunit"])
D,Dunit = check_units(D,Dunit,layer._defaults["Dunit"])
k,kunit = check_units(k,kunit,layer._defaults["kunit"])
C0,Cunit = check_units(C0,Cunit,layer._defaults["Cunit"])
rho,rhounit = check_units(rho,rhounit,layer._defaults["rhounit"])
T,Tunit = check_units(T,Tunit,layer._defaults["Tunit"])
# set attributes: id and physical properties
self._name = [layername]
self._type = [layertype]
self._material = [layermaterial]
self._code = [layercode]
self._nlayer = 1
self._l = l[:1]
self._D = D[:1]
self._k = k[:1]
self._C0 = C0[:1]
self._rho = rho[:1]
self._T = T
self._lunit = lunit
self._Dunit = Dunit
self._kunit = kunit
self._Cunit = Cunit
self._rhounit = rhounit
self._Tunit = Tunit
self._nmesh = nmesh
self._nmeshmin = nmeshmin
# intialize links for X = D,k,C0,T,l (see documentation of layerLink)
# A link enables the values of X to be defined and controlled outside the instance
self._Dlink = self._initialize_link(Dlink, "D")
self._klink = self._initialize_link(klink, "k")
self._C0link = self._initialize_link(C0link, "C0")
self._Tlink = self._initialize_link(Tlink, "T")
self._llink = self._initialize_link(llink, "l")
# set substance, medium and related D and k models
if isinstance(substance,str):
substance = migrant(substance)
if substance is not None and not isinstance(substance,migrant):
raise ValueError(f"subtance must be None a or a migrant not a {type(substance).__name__}")
self._substance = substance
if medium is not None:
from patankar.food import foodlayer # local import only if needed
if not isinstance(medium,foodlayer):
raise ValueError(f"medium must be None or a foodlayer not a {type(medium).__name__}")
self._medium = medium
self._Dmodel = "default" # do not use directly self._compute_Dmodel (force refresh)
self._kmodel = "default" # do not use directly self._compute_kmodel (force refresh)
# set history for all layers merged with +
self._layerclass_history = []
self._ispolymer_history = []
self._chemicalsubstance_history = []
# set verbosity attributes
self.verbosity = 0 if verbosity is None else verbosity
self.verbose = verbosity>0 if verbose is None else verbose
# we initialize the acknowlegment process for future property propagation
self._hasbeeninherited = {}
# --------------------------------------------------------------------
# Helper method: initializes and validates layerLink attributes
# (Dlink, klink, C0link, Tlink, llink)
# --------------------------------------------------------------------
def _initialize_link(self, link, expected_property):
"""
Initializes and validates a layerLink attribute.
Parameters:
----------
link : layerLink or None
The `layerLink` instance to be assigned.
expected_property : str
The expected property name (e.g., "D", "k", "C0", "T").
Returns:
-------
layerLink or None
The validated `layerLink` instance or None.
Raises:
-------
TypeError:
If `link` is not a `layerLink` or `None`.
ValueError:
If `link.property` does not match `expected_property`.
"""
if link is None:
return None
if isinstance(link, layerLink):
if link.property == expected_property:
return link
raise ValueError(f'{expected_property}link.property should be "{expected_property}" not "{link.property}"')
raise TypeError(f"{expected_property}link must be a layerLink not a {type(link).__name__}")
# --------------------------------------------------------------------
# Class method returning help() for the end user
# --------------------------------------------------------------------
@classmethod
def help(cls):
"""
Prints a dynamically formatted summary of all input parameters,
adjusting column widths based on content and wrapping long descriptions.
"""
# Column Headers
headers = ["Parameter", "Default Value", "Has Synonyms?", "Description"]
col_widths = [len(h) for h in headers] # Start with header widths
# Collect Data Rows
rows = []
for param, default in cls._defaults.items():
has_synonyms = "✅ Yes" if param in cls._synonyms else "❌ No"
description = cls._descriptionInputs.get(param, "No description available")
# Update column widths dynamically
col_widths[0] = max(col_widths[0], len(param))
col_widths[1] = max(col_widths[1], len(str(default)))
col_widths[2] = max(col_widths[2], len(has_synonyms))
col_widths[3] = max(col_widths[3], len(description))
rows.append([param, str(default), has_synonyms, description])
# Function to wrap text for a given column width
def wrap_text(text, width):
return textwrap.fill(text, width)
# Print Table with Adjusted Column Widths
separator = "+-" + "-+-".join("-" * w for w in col_widths) + "-+"
print("\n### **Accepted Parameters and Defaults**\n")
print(separator)
print("| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + " |")
print(separator)
for row in rows:
# Wrap text in the description column
row[3] = wrap_text(row[3], col_widths[3])
# Print row
print("| " + " | ".join(row[i].ljust(col_widths[i]) for i in range(3)) + " | " + row[3])
print(separator)
# Synonyms Table
print("\n### **Parameter Synonyms**\n")
syn_headers = ["Parameter", "Synonyms"]
syn_col_widths = [
max(len("Parameter"), max(len(k) for k in cls._synonyms.keys())), # Ensure it fits "Parameter"
max(len("Synonyms"), max(len(", ".join(v)) for v in cls._synonyms.values())) # Ensure it fits "Synonyms"
]
syn_separator = "+-" + "-+-".join("-" * w for w in syn_col_widths) + "-+"
print(syn_separator)
print("| " + " | ".join(h.ljust(syn_col_widths[i]) for i, h in enumerate(syn_headers)) + " |")
print(syn_separator)
for param, synonyms in cls._synonyms.items():
print(f"| {param.ljust(syn_col_widths[0])} | {', '.join(synonyms).ljust(syn_col_widths[1])} |")
print(syn_separator)
# --------------------------------------------------------------------
# Class method to handle ambiguous definitions from end-user
# --------------------------------------------------------------------
@classmethod
def resolvename(cls, param_value, param_key, **unresolved):
"""
Resolves the correct parameter value using known synonyms.
- If param_value is already set (not None), return it.
- If a synonym exists in **unresolved, assign its value.
- If multiple synonyms of the same parameter appear in **unresolved, raise an error.
- Otherwise, return None.
Parameters:
- `param_name` (any): The original value (if provided).
- `param_key` (str): The legitimate parameter name we are resolving.
- `unresolved` (dict): The dictionary of unrecognized keyword arguments.
Returns:
- The resolved value or None if not found.
"""
if param_value is not None:
return param_value # The parameter is explicitly defined, do not override
if not unresolved: # shortcut
return None
resolved_value = None
found_keys = []
# Check if param_key itself is present in unresolved
if param_key in unresolved:
found_keys.append(param_key)
resolved_value = unresolved[param_key]
# Check if any of its synonyms are in unresolved
if param_key in cls._synonyms:
for synonym in cls._synonyms[param_key]:
if synonym in unresolved:
found_keys.append(synonym)
resolved_value = unresolved[synonym]
# Raise error if multiple synonyms were found
if len(found_keys) > 1:
raise ValueError(
f"Conflicting definitions: Multiple synonyms {found_keys} were provided for '{param_key}'."
)
return resolved_value
# --------------------------------------------------------------------
# overloading binary addition (note that the output is of type layer)
# --------------------------------------------------------------------
def __add__(self, other):
""" C = A + B | overload + operator """
if isinstance(other, layer):
res = duplicate(self)
res._nmeshmin = min(self._nmeshmin, other._nmeshmin)
# Propagate substance
if self._substance is None:
res._substance = other._substance
else:
if isinstance(self._substance, migrant) and isinstance(other._substance, migrant):
if self._substance.M != other._substance.M:
print("Warning: the smallest substance is propagated everywhere")
res._substance = self._substance if self._substance.M <= other._substance.M else other._substance
else:
res._substance = None
# Concatenate general attributes
for p in ["_name", "_type", "_material", "_code", "_nlayer"]:
setattr(res, p, getattr(self, p) + getattr(other, p))
# Concatenate numeric arrays
for p in ["_l", "_D", "_k", "_C0", "_rho", "_T"]:
setattr(res, p, np.concatenate((getattr(self, p), getattr(other, p))))
# Handle history tracking
res._layerclass_history = self.layerclass_history + other.layerclass_history
res._ispolymer_history = self.ispolymer_history + other.ispolymer_history
res._chemicalsubstance_history = self.chemicalsubstance_history + other.chemicalsubstance_history
# Manage layerLink attributes (Dlink, klink, C0link, Tlink, llink)
property_map = {
"Dlink": ("D", self.Dlink, other.Dlink),
"klink": ("k", self.klink, other.klink),
"C0link": ("C0", self.C0link, other.C0link),
"Tlink": ("T", self.Tlink, other.Tlink),
"llink": ("l", self.llink, other.llink),
}
for attr, (prop, self_link, other_link) in property_map.items():
if (self_link is not None) and (other_link is not None):
# Case 1: Both have a link → Apply `+`
setattr(res, '_'+attr, self_link + other_link)
elif self_link is not None:
# Case 2: Only self has a link → Use as-is
setattr(res, '_'+attr, self_link)
elif other_link is not None:
# Case 3: Only other has a link → Shift indices and use
shifted_link = duplicate(other_link)
shifted_link.indices += len(getattr(self, prop))
setattr(res, '_'+attr, shifted_link)
else:
# Case 4: Neither has a link → Result is None
setattr(res, '_'+attr, None)
return res
else:
raise ValueError("Invalid layer object")
# --------------------------------------------------------------------
# overloading binary multiplication (note that the output is of type layer)
# --------------------------------------------------------------------
def __mul__(self,ntimes):
""" nA = A*n | overload * operator """
if isinstance(ntimes, int) and ntimes>0:
res = duplicate(self)
if ntimes>1:
for n in range(1,ntimes): res += self
return res
else: raise ValueError("multiplicator should be a strictly positive integer")
# --------------------------------------------------------------------
# len method
# --------------------------------------------------------------------
def __len__(self):
""" length method """
return self._nlayer
# --------------------------------------------------------------------
# object indexing (get,set) method
# --------------------------------------------------------------------
def __getitem__(self,i):
""" get indexing method """
res = duplicate(self)
# check indices
isscalar = isinstance(i,int)
if isinstance(i,slice):
if i.step==None: j = list(range(i.start,i.stop))
else: j = list(range(i.start,i.stop,i.step))
res._nlayer = len(j)
if isinstance(i,int): res._nlayer = 1
# pick indices for each property
for p in ["_name","_type","_material","_l","_D","_k","_C0"]:
content = getattr(self,p)
try:
if isscalar: setattr(res,p,content[i:i+1])
else: setattr(res,p,content[i])
except IndexError as err:
if self.verbosity>0 and self.verbose:
print("bad layer object indexing: ",err)
return res
def __setitem__(self,i,other):
""" set indexing method """
# check indices
if isinstance(i,slice):
if i.step==None: j = list(range(i.start,i.stop))
else: j = list(range(i.start,i.stop,i.step))
elif isinstance(i,int): j = [i]
else:raise IndexError("invalid index")
islayer = isinstance(other,layer)
isempty = not islayer and isinstance(other,list) and len(other)<1
if isempty: # empty right hand side
for p in ["_name","_type","_material","_l","_D","_k","_C0"]:
content = getattr(self,p)
try:
newcontent = [content[k] for k in range(self._nlayer) if k not in j]
except IndexError as err:
if self.verbosity>0 and self.verbose:
print("bad layer object indexing: ",err)
if isinstance(content,np.ndarray) and not isinstance(newcontent,np.ndarray):
newcontent = np.array(newcontent)
setattr(self,p,newcontent)
self._nlayer = len(newcontent)
elif islayer: # islayer right hand side
nk1 = len(j)
nk2 = other._nlayer
if nk1 != nk2:
raise IndexError("the number of elements does not match the number of indices")
for p in ["_name","_type","_material","_l","_D","_k","_C0"]:
content1 = getattr(self,p)
content2 = getattr(other,p)
for k in range(nk1):
try:
content1[j[k]] = content2[k]
except IndexError as err:
if self.verbosity>0 and self.verbose:
print("bad layer object indexing: ",err)
setattr(self,p,content1)
else:
raise ValueError("only [] or layer object are accepted")
# --------------------------------------------------------------------
# Getter methods (show private/hidden properties and meta-properties)
# --------------------------------------------------------------------
# Return class or instance attributes
@property
def physicalstate(self): return self._physicalstate
@property
def chemicalclass(self): return self._chemicalclass
@property
def chemicalsubstance(self): return self._chemicalsubstance
@property
def polarityindex(self):
# rescaled to match predictions - standard scale [0,10.2] - predicted scale [0,7.12]
return self._polarityindex * migrant("water").polarityindex/10.2
@property
def ispolymer(self): return self.chemicalclass == "polymer"
@property
def issolid(self): return self.physicalstate == "solid"
@property
def layerclass_history(self):
return self._layerclass_history if self._layerclass_history != [] else [self.layerclass]
@property
def ispolymer_history(self):
return self._ispolymer_history if self._ispolymer_history != [] else [self.ispolymer]
@property
def chemicalsubstance_history(self):
return self._chemicalsubstance_history if self._chemicalsubstance_history != [] else [self.chemicalsubstance]
@property
def layerclass(self): return type(self).__name__
@property
def name(self): return self._name
@property
def type(self): return self._type
@property
def material(self): return self._material
@property
def code(self): return self._code
@property
def l(self): return self._l if not self.hasllink else self.llink.getfull(self._l)
@property
def D(self):
Dtmp = None
if self.Dmodel == "default": # default behavior
Dtmp = self._compute_Dmodel()
elif callable(self.Dmodel): # user override
Dtmp = self.Dmodel()
if Dtmp is not None:
Dtmp = np.full_like(self._D, Dtmp,dtype=np.float64)
if self.hasDlink:
return self.Dlink.getfull(Dtmp) # substitution rules are applied as defined in Dlink
else:
return Dtmp
return self._D if not self.hasDlink else self.Dlink.getfull(self._D)
@property
def k(self):
ktmp = None
if self.kmodel == "default": # default behavior
ktmp = self._compute_kmodel()
elif callable(self.kmodel): # user override
ktmp = self.kmodel()
if ktmp is not None:
ktmp = np.full_like(self._k, ktmp,dtype=np.float64)
if self.hasklink:
return self.klink.getfull(ktmp) # substitution rules are applied as defined in klink
else:
return ktmp
return self._k if not self.hasklink else self.klink.getfull(self._k)
@property
def C0(self): return self._C0 if not self.hasC0link else self.COlink.getfull(self._C0)
@property
def rho(self): return self._rho
@property
def T(self): return self._T if not self.hasTlink else self.Tlink.getfull(self._T)
@property
def TK(self): return self._T+T0K
@property
def lunit(self): return self._lunit
@property
def Dunit(self): return self._Dunit
@property
def kunit(self): return self._kunit
@property
def Cunit(self): return self._Cunit
@property
def rhounit(self): return self._rhounit
@property
def Tunit(self): return self._Tunit
@property
def TKunit(self): return "K"
@property
def n(self): return self._nlayer
@property
def nmesh(self): return self._nmesh
@property
def nmeshmin(self): return self._nmeshmin
@property
def resistance(self): return self.l*self.k/self.D
@property
def permeability(self): return self.D/(self.l*self.k)
@property
def lag(self): return self.l**2/(6*self.D)
@property
def pressure(self): return self.k*self.C0
@property
def thickness(self): return sum(self.l)
@property
def concentration(self): return sum(self.l*self.C0)/self.thickness
@property
def relative_thickness(self): return self.l/self.thickness
@property
def relative_resistance(self): return self.resistance/sum(self.resistance)
@property
def rank(self): return (self.n-np.argsort(np.array(self.resistance))).tolist()
@property
def referencelayer(self): return np.argmax(self.resistance)
@property
def lreferencelayer(self): return self.l[self.referencelayer]
@property
def Foscale(self): return self.D[self.referencelayer]/self.lreferencelayer**2
# substance/solute/migrant/chemical (of class migrant or None)
@property
def substance(self): return self._substance
@property
def migrant(self): return self.substance # alias/synonym of substance
@property
def solute(self): return self.substance # alias/synonym of substance
@property
def chemical(self): return self.substance # alias/synonym of substance
# medium (of class foodlayer or None)
@property
def medium(self): return self._medium
# Dmodel and kmodel returned as properties (they are lambda functions)
# Note about the implementation: They are attributes that remain None or a callable function
# polymer and mass are udpdated on the fly (the code loops over all layers)
@property
def Dmodel(self):
return self._Dmodel
@Dmodel.setter
def Dmodel(self,value):
if value is None or callable(value):
self._Dmodel = value
else:
raise ValueError("Dmodel must be None or a callable function")
@property
def _compute_Dmodel(self):
"""Return a callable function that evaluates D with updated parameters."""
if not isinstance(self._substance,migrant) or self._substance.Deval() is None:
return lambda **kwargs: None # Return a function that always returns None
template = self._substance.Dtemplate.copy()
template.update()
def func(**kwargs):
D = np.empty_like(self._D)
for (i,),T in np.ndenumerate(self.T.ravel()): # loop over all layers via T
template.update(polymer=self.layerclass_history[i],T=T) # updated layer properties
# inherit eventual user parameters
D[i] = self._substance.D.evaluate(**dict(template, **kwargs))
return D
return func # we return a callable function not a value
# polarity index and molar volume are updated on the fly
@property
def kmodel(self):
return self._kmodel
@kmodel.setter
def kmodel(self,value):
if value is None or callable(value):
self._kmodel = value
else:
raise ValueError("kmodel must be None or a callable function")
@property
def _compute_kmodel(self):
"""Return a callable function that evaluates k with updated parameters."""
if not isinstance(self._substance,migrant) or self._substance.keval() is None:
return lambda **kwargs: None # Return a function that always returns None
template = self._substance.ktemplate.copy()
# add solute (i) properties: Pi and Vi have been set by loadpubchem already
template.update(ispolymer = True)
def func(**kwargs):
k = np.full_like(self._k,self._k,dtype=np.float64)
for (i,),T in np.ndenumerate(self.T.ravel()): # loop over all layers via T
if not self.ispolymer_history[i]: # k can be evaluated only in polymes via FH theory
continue # we keep the existing k value
# add/update monomer properties
monomer = migrant(self.chemicalsubstance_history[i])
template.update(Pk = monomer.polarityindex,
Vk = monomer.molarvolumeMiller)
# inherit eventual user parameters
k[i] = self._substance.k.evaluate(**dict(template, **kwargs))
return k
return func # we return a callable function not a value
@property
def hasDmodel(self):
"""Returns True if a Dmodel has been defined"""
if hasattr(self, "_compute_Dmodel"):
if self._compute_Dmodel() is not None:
return True
elif callable(self.Dmodel):
return self.Dmodel() is not None
return False
@property
def haskmodel(self):
"""Returns True if a kmodel has been defined"""
if hasattr(self, "_compute_kmodel"):
if self._compute_kmodel() is not None:
return True
elif callable(self.kmodel):
return self.kmodel() is not None
return False
# --------------------------------------------------------------------
# comparators based resistance
# --------------------------------------------------------------------
def __eq__(self, o):
value1 = self.resistance if self._nlayer>1 else self.resistance[0]
if isinstance(o,layer):
value2 = o.resistance if o._nlayer>1 else o.resistance[0]
else:
value2 = o
return value1==value2
def __ne__(self, o):
value1 = self.resistance if self._nlayer>1 else self.resistance[0]
if isinstance(o,layer):
value2 = o.resistance if o._nlayer>1 else o.resistance[0]
else:
value2 = o
return value1!=value2
def __lt__(self, o):
value1 = self.resistance if self._nlayer>1 else self.resistance[0]
if isinstance(o,layer):
value2 = o.resistance if o._nlayer>1 else o.resistance[0]
else:
value2 = o
return value1<value2
def __gt__(self, o):
value1 = self.resistance if self._nlayer>1 else self.resistance[0]
if isinstance(o,layer):
value2 = o.resistance if o._nlayer>1 else o.resistance[0]
else:
value2 = o
return value1>value2
def __le__(self, o):
value1 = self.resistance if self._nlayer>1 else self.resistance[0]
if isinstance(o,layer):
value2 = o.resistance if o._nlayer>1 else o.resistance[0]
else:
value2 = o
return value1<=value2
def __ge__(self, o):
value1 = self.resistance if self._nlayer>1 else self.resistance[0]
if isinstance(o,layer):
value2 = o.resistance if o._nlayer>1 else o.resistance[0]
else:
value2 = o
return value1>=value2
# --------------------------------------------------------------------
# Generates mesh
# --------------------------------------------------------------------
def mesh(self,nmesh=None,nmeshmin=None):
""" nmesh() generates mesh based on nmesh and nmeshmin, nmesh(nmesh=value,nmeshmin=value) """
if nmesh==None: nmesh = self.nmesh
if nmeshmin==None: nmeshmin = self.nmeshmin
if nmeshmin>nmesh: nmeshmin,nmesh = nmesh, nmeshmin
# X = mesh distribution (number of nodes per layer)
X = np.ones(self._nlayer)
for i in range(1,self._nlayer):
X[i] = X[i-1]*(self.permeability[i-1]*self.l[i])/(self.permeability[i]*self.l[i-1])
X = np.maximum(nmeshmin,np.ceil(nmesh*X/sum(X)))
X = np.round((X/sum(X))*nmesh).astype(int)
# do the mesh
x0 = 0
mymesh = []
for i in range(self._nlayer):
mymesh.append(mesh(self.l[i]/self.l[self.referencelayer],X[i],x0=x0,index=i))
x0 += self.l[i]
return mymesh
# --------------------------------------------------------------------
# Setter methods and tools to validate inputs checknumvalue and checktextvalue
# --------------------------------------------------------------------
@physicalstate.setter
def physicalstate(self,value):
if value not in ("solid","liquid","gas","supercritical"):
raise ValueError(f"physicalstate must be solid/liduid/gas/supercritical and not {value}")
self._physicalstate = value
@chemicalclass.setter
def chemicalclass(self,value):
if value not in ("polymer","other"):
raise ValueError(f"chemicalclass must be polymer/oher and not {value}")
self._chemicalclass= value
@chemicalsubstance.setter
def chemicalsubstance(self,value):
if not isinstance(value,str):
raise ValueError("chemicalsubtance must be str not a {type(value).__name__}")
self._chemicalsubstance= value
@polarityindex.setter
def polarityindex(self,value):
if not isinstance(value,(float,int)):
raise ValueError("polarity index must be float not a {type(value).__name__}")
self._polarityindex= value
def checknumvalue(self,value,ExpectedUnits=None):
""" returns a validate value to set properties """
if isinstance(value,tuple):
value = check_units(value,ExpectedUnits=ExpectedUnits)
if isinstance(value,int): value = float(value)
if isinstance(value,float): value = np.array([value])
if isinstance(value,list): value = np.array(value)
if len(value)>self._nlayer:
value = value[:self._nlayer]
if self.verbosity>1 and self.verbose:
print('dimension mismatch, the extra value(s) has been removed')
elif len(value)<self._nlayer:
value = np.concatenate((value,value[-1:]*np.ones(self._nlayer-len(value))))
if self.verbosity>1 and self.verbose:
print('dimension mismatch, the last value has been repeated')
return value
def checktextvalue(self,value):
""" returns a validate value to set properties """
if not isinstance(value,list): value = [value]
if len(value)>self._nlayer:
value = value[:self._nlayer]
if self.verbosity>1 and self.verbose:
print('dimension mismatch, the extra entry(ies) has been removed')
elif len(value)<self._nlayer:
value = value + value[-1:]*(self._nlayer-len(value))
if self.verbosity>1 and self.verbose:
print('dimension mismatch, the last entry has been repeated')
return value
@l.setter
def l(self,value): self._l =self.checknumvalue(value,layer._defaults["lunit"])
@D.setter
def D(self,value): self._D=self.checknumvalue(value,layer._defaults["Dunit"])
@k.setter
def k(self,value): self._k =self.checknumvalue(value,layer._defaults["kunit"])
@C0.setter
def C0(self,value): self._C0 =self.checknumvalue(value,layer._defaults["Cunit"])
@rho.setter
def rho(self,value): self._rho =self.checknumvalue(value,layer._defaults["rhounit"])
@T.setter
def T(self,value): self._T =self.checknumvalue(value,layer._defaults["Tunit"])
@name.setter
def name(self,value): self._name =self.checktextvalue(value)
@type.setter
def type(self,value): self._type =self.checktextvalue(value)
@material.setter
def material(self,value): self._material =self.checktextvalue(value)
@nmesh.setter
def nmesh(self,value): self._nmesh = max(value,self._nlayer*self._nmeshmin)
@nmeshmin.setter
def nmeshmin(self,value): self._nmeshmin = max(value,round(self._nmesh/(2*self._nlayer)))
@substance.setter
def substance(self,value):
if isinstance(value,str):
value = migrant(value)
if not isinstance(value,migrant) and value is not None:
raise TypeError(f"value must be a migrant not a {type(value).__name__}")
self._substance = value
@migrant.setter
def migrant(self,value):
self.substance = value
@chemical.setter
def chemical(self,value):
self.substance = value
@solute.setter
def solute(self,value):
self.substance = value
@medium.setter
def medium(self,value):
from patankar.food import foodlayer
if not isinstance(value,foodlayer):
raise TypeError(f"value must be a foodlayer not a {type(value).__name__}")
self._medium = value
# --------------------------------------------------------------------
# getter and setter for links: Dlink, klink, C0link, Tlink, llink
# --------------------------------------------------------------------
@property
def Dlink(self):
"""Getter for Dlink"""
return self._Dlink
@Dlink.setter
def Dlink(self, value):
"""Setter for Dlink"""
self._Dlink = self._initialize_link(value, "D")
if isinstance(value,layerLink): value._maxlength = self.n
@property
def klink(self):
"""Getter for klink"""
return self._klink
@klink.setter
def klink(self, value):
"""Setter for klink"""
self._klink = self._initialize_link(value, "k")
if isinstance(value,layerLink): value._maxlength = self.n
@property
def C0link(self):
"""Getter for C0link"""
return self._C0link
@C0link.setter
def C0link(self, value):
"""Setter for C0link"""
self._C0link = self._initialize_link(value, "C0")
if isinstance(value,layerLink): value._maxlength = self.n
@property
def Tlink(self):
"""Getter for Tlink"""
return self._Tlink
@Tlink.setter
def Tlink(self, value):
"""Setter for Tlink"""
self._Tlink = self._initialize_link(value, "T")
if isinstance(value,layerLink): value._maxlength = self.n
@property
def llink(self):
"""Getter for llink"""
return self._llink
@llink.setter
def llink(self, value):
"""Setter for llink"""
self._llink = self._initialize_link(value, "l")
if isinstance(value,layerLink): value._maxlength = self.n
@property
def hasDlink(self):
"""Returns True if Dlink is defined"""
return self.Dlink is not None
@property
def hasklink(self):
"""Returns True if klink is defined"""
return self.klink is not None
@property
def hasC0link(self):
"""Returns True if C0link is defined"""
return self.C0link is not None
@property
def hasTlink(self):
"""Returns True if Tlink is defined"""
return self.Tlink is not None
@property
def hasllink(self):
"""Returns True if llink is defined"""
return self.llink is not None
# --------------------------------------------------------------------
# returned LaTeX-formated properties
# --------------------------------------------------------------------
def Dlatex(self, numdigits=4, units=r"\mathrm{m^2 \cdot s^{-1}}",prefix="D=",mathmode="$"):
"""Returns diffusivity values (D) formatted in LaTeX scientific notation."""
return [format_scientific_latex(D, numdigits, units, prefix,mathmode) for D in self.D]
def klatex(self, numdigits=4, units="a.u.",prefix="k=",mathmode="$"):
"""Returns Henry-like values (k) formatted in LaTeX scientific notation."""
return [format_scientific_latex(k, numdigits, units, prefix,mathmode) for k in self.k]
def llatex(self, numdigits=4, units="m",prefix="l=",mathmode="$"):
"""Returns thickness values (k) formatted in LaTeX scientific notation."""
return [format_scientific_latex(l, numdigits, units, prefix,mathmode) for l in self.l]
def C0latex(self, numdigits=4, units="a.u.",prefix="C0=",mathmode="$"):
"""Returns Initial Concentratoin values (C0) formatted in LaTeX scientific notation."""
return [format_scientific_latex(c, numdigits, units, prefix,mathmode) for c in self.C0]
# --------------------------------------------------------------------
# hash methods (assembly and layer-by-layer)
# note that list needs to be converted into tuples to be hashed
# --------------------------------------------------------------------
def __hash__(self):
""" hash layer-object (assembly) method """
return hash((tuple(self._name),
tuple(self._type),
tuple(self._material),
tuple(self._l),
tuple(self._D),
tuple(self.k),
tuple(self._C0),
tuple(self._rho)))
# layer-by-layer @property = decoration to consider it
# as a property instead of a method/attribute
# comprehension for n in range(self._nlayer) applies it to all layers
@property
def hashlayer(self):
""" hash layer (layer-by-layer) method """
return [hash((self._name[n],
self._type[n],
self._material[n],
self._l[n],
self._D[n],
self.k[n],
self._C0[n],
self._rho[n]))
for n in range(self._nlayer)
]
# --------------------------------------------------------------------
# repr method (since the getter are defined, the '_' is dropped)
# --------------------------------------------------------------------
# density and temperature are not shown
def __repr__(self):
""" disp method """
print("\n[%s version=%0.4g, contact=%s]" % (self.__description,self.__version,self.__contact))
if self._nlayer==0:
print("empty %s" % (self.__description))
else:
hasDmodel, haskmodel = self.hasDmodel, self.haskmodel
hasDlink, hasklink, hasC0link, hasTlink, hasllink = self.hasDlink, self.hasklink, self.hasC0link, self.hasTlink, self.hasllink
properties_hasmodel = {"l":False,"D":hasDmodel,"k":haskmodel,"C0":False}
properties_haslink = {"l":hasllink,"D":hasDlink,"k":hasklink,"C0":hasC0link,"T":hasTlink}
if hasDmodel or haskmodel:
properties_hasmodel["T"] = False
fmtval = '%10s: '+self._printformat+" [%s]"
fmtstr = '%10s= %s'
if self._nlayer==1:
print(f'monolayer of {self.__description}:')
else:
print(f'{self._nlayer}-multilayer of {self.__description}:')
for n in range(1,self._nlayer+1):
modelinfo = {
"D": f"{self._substance.D.__name__}({self.layerclass_history[n-1]},{self._substance},T={float(self.T[0])} {self.Tunit})" if hasDmodel else "",
"k": f"{self._substance.k.__name__}(<{self.chemicalsubstance_history[n-1]}>,{self._substance})" if haskmodel else "",
}
print('-- [ layer %d of %d ] ---------- barrier rank=%d --------------'
% (n,self._nlayer,self.rank[n-1]))
for p in ["name","type","material","code"]:
v = getattr(self,p)
print('%10s: "%s"' % (p,v[n-1]),flush=True)
for p in properties_hasmodel.keys():
v = getattr(self,p) # value
vunit = getattr(self,p[0]+"unit") # value unit
print(fmtval % (p,v[n-1],vunit),flush=True)
isoverridenbylink = False
if properties_haslink[p]:
isoverridenbylink = not np.isnan(getattr(self,p+"link").get(n-1))
if isoverridenbylink:
print(fmtstr % ("",f"value controlled by {p}link[{n-1}] (external)"),flush=True)
elif properties_hasmodel[p]:
print(fmtstr % ("",modelinfo[p]),flush=True)
return str(self)
def __str__(self):
"""Formatted string representation of layer"""
all_identical = len(set(self.layerclass_history)) == 1
cls = self.__class__.__name__ if all_identical else "multilayer"
return f"<{cls} with {self.n} layer{'s' if self.n>1 else ''}: {self.name}>"
# --------------------------------------------------------------------
# Returns the equivalent dictionary from an object for debugging
# --------------------------------------------------------------------
def _todict(self):
""" returns the equivalent dictionary from an object """
return dict((key, getattr(self, key)) for key in dir(self) if key not in dir(self.__class__))
# --------------------------------------------------------------------
# --------------------------------------------------------------------
# Simplify layers by collecting similar ones
# --------------------------------------------------------------------
def simplify(self):
""" merge continuous layers of the same type """
nlayer = self._nlayer
if nlayer>1:
res = self[0]
ires = 0
ireshash = res.hashlayer[0]
for i in range(1,nlayer):
if self.hashlayer[i]==ireshash:
res.l[ires] = res.l[ires]+self.l[i]
else:
res = res + self[i]
ires = ires+1
ireshash = self.hashlayer[i]
else:
res = self.copy()
return res
# --------------------------------------------------------------------
# Split layers into a tuple
# --------------------------------------------------------------------
def split(self):
""" split layers """
out = ()
if self._nlayer>0:
for i in range(self._nlayer):
out = out + (self[i],) # (,) special syntax for tuple singleton
return out
# --------------------------------------------------------------------
# deepcopy
# --------------------------------------------------------------------
def copy(self,**kwargs):
"""
Creates a deep copy of the current layer instance.
Returns:
- layer: A new layer instance identical to the original.
"""
return duplicate(self).update(**kwargs)
# --------------------------------------------------------------------
# update contact conditions from a foodphysics instance (or do the reverse)
# material << medium
# material@medium
# --------------------------------------------------------------------
def _from(self,medium=None):
"""Propagates contact conditions from food instance"""
from patankar.food import foodphysics, foodlayer
if not isinstance(medium,foodphysics):
raise TypeError(f"medium must be a foodphysics, foodlayer not a {type(medium).__name__}")
if not hasattr(medium, "contacttemperature"):
medium.contacttemperature = self.T[0]
T = medium.get_param("contacttemperature",40,acceptNone=False)
self.T = np.full_like(self.T,T,dtype=np.float64)
if medium.substance is not None:
self.substance = medium.substance
else:
medium.substance = self.substance # do the reverse if substance is not defined in medium
# inherit fully medium only if it is a foodlayer (foodphysics is too restrictive)
if isinstance(medium,foodlayer):
self.medium = medium
# overload operator <<
def __lshift__(self, medium):
"""Overloads << to propagate contact conditions from food."""
self._from(medium)
# overload operator @ (same as <<)
def __matmul__(self, medium):
"""Overloads @ to propagate contact conditions from food."""
self._from(medium)
# --------------------------------------------------------------------
# Inheritance registration mechanism associated with food >> layer
# It is used by food, not by layer (please refer to food.py).
# Note that layer >> food means mass transfer simulation
# --------------------------------------------------------------------
def acknowledge(self, what=None, category=None):
"""
Register inherited properties under a given category.
Parameters:
-----------
what : str or list of str or a set
The properties or attributes that have been inherited.
category : str
The category under which the properties are grouped.
"""
if category is None or what is None:
raise ValueError("Both 'what' and 'category' must be provided.")
if isinstance(what, str):
what = {what} # Convert string to a set
elif isinstance(what, list):
what = set(what) # Convert list to a set for uniqueness
elif not isinstance(what,set):
raise TypeError("'what' must be a string, a list, or a set of strings.")
if category not in self._hasbeeninherited:
self._hasbeeninherited[category] = set()
self._hasbeeninherited[category].update(what)
# --------------------------------------------------------------------
# migration simulation overloaded as sim = layer >> food
# using layer >> food without output works also.
# The result is stored in food.lastsimulation
# --------------------------------------------------------------------
def contact(self,medium,**kwargs):
"""alias to migration method"""
return self.migration(medium,**kwargs)
def migration(self,medium=None,**kwargs):
"""interface to simulation engine: senspantankar"""
from patankar.food import foodphysics
from patankar.migration import senspatankar
if medium is None:
medium = self.medium
if not isinstance(medium,foodphysics):
raise TypeError(f"medium must be a foodphysics not a {type(medium).__name__}")
sim = senspatankar(self,medium,**kwargs)
medium.lastsimulation = sim # store the last simulation result in medium
medium.lastinput = self # store the last input (self)
sim.savestate(self,medium) # store store the inputs in sim for chaining
return sim
# overloading operation
def __rshift__(self, medium):
"""Overloads >> to propagate migration to food."""
from patankar.food import foodphysics
if not isinstance(medium,foodphysics):
raise TypeError(f"medium must be a foodphysics object not a {type(medium).__name__}")
return self.contact(medium)
# --------------------------------------------------------------------
# Safe update method
# --------------------------------------------------------------------
def update(self, **kwargs):
"""
Update layer parameters following strict validation rules.
Rules:
1) key should be listed in self._defaults
2) for some keys, synonyms are acceptable as reported in self._synonyms
3) values cannot be None if they were not None in _defaults
4) values should be str if they were initially str, idem with bool
5) values which were numeric (int, float, np.ndarray) should remain numeric.
6) lists are acceptable as numeric arrays
7) all numerical (float, np.ndarray, list) except int must be converted into numpy arrays.
Values which were int in _defaults must remain int and an error should be raised
if a float value is proposed.
8) keys listed in _parametersWithUnits can be assigned with tuples (value, "unit").
They will be converted automatically with check_units(value).
9) for parameters with a default value None, any value is acceptable
10) A clear error message should be displayed for any bad value showing the
current value of the parameter and its default value.
"""
if not kwargs: # shortcut
return self # for chaining
param_counts = {key: 0 for key in self._defaults} # Track how many times each param is set
def resolve_key(key):
"""Resolve key considering synonyms and check for duplicates."""
for main_key, synonyms in self._synonyms.items():
if key == main_key or key in synonyms:
param_counts[main_key] += 1
return main_key
param_counts[key] += 1
return key
def validate_value(key, value):
"""Validate and process the value according to the rules."""
default_value = self._defaults[key]
# Rule 3: values cannot be None if they were not None in _defaults
if value is None and default_value is not None:
raise ValueError(f"Invalid value for '{key}': None is not allowed. "
f"Current: {getattr(self, key)}, Default: {default_value}")
# Rule 9: If default is None, any value is acceptable
if default_value is None:
return value
# Rule 4 & 5: Ensure type consistency (str, bool, or numeric types)
if isinstance(default_value, str) and not isinstance(value, str):
raise TypeError(f"Invalid type for '{key}': Expected str, got {type(value).__name__}. "
f"Current: {getattr(self, key)}, Default: {default_value}")
if isinstance(default_value, bool) and not isinstance(value, bool):
raise TypeError(f"Invalid type for '{key}': Expected bool, got {type(value).__name__}. "
f"Current: {getattr(self, key)}, Default: {default_value}")
# Rule 6 & 7: Convert numeric types properly
if isinstance(default_value, (int, float, np.ndarray)):
if isinstance(value, list):
value = np.array(value)
if isinstance(default_value, int):
if isinstance(value, float) or (isinstance(value, np.ndarray) and np.issubdtype(value.dtype, np.floating)):
raise TypeError(f"Invalid type for '{key}': Expected integer, got float. "
f"Current: {getattr(self, key)}, Default: {default_value}")
if isinstance(value, (int, np.integer)):
return int(value) # Ensure it remains an int
raise TypeError(f"Invalid type for '{key}': Expected integer, got {type(value).__name__}. "
f"Current: {getattr(self, key)}, Default: {default_value}")
if isinstance(value, (int, float, list, np.ndarray)):
return np.array(value, dtype=float) # Convert everything to np.array for floats
raise TypeError(f"Invalid type for '{key}': Expected numeric, got {type(value).__name__}. "
f"Current: {getattr(self, key)}, Default: {default_value}")
# Rule 8: Convert units if applicable
if key in self._parametersWithUnits and isinstance(value, tuple):
value, unit = value
converted_value, _ = check_units((value, unit), ExpectedUnits=self._parametersWithUnits[key])
return converted_value
return value
# Apply updates while tracking parameter occurrences
for key, value in kwargs.items():
resolved_key = resolve_key(key)
if resolved_key not in self._defaults:
raise KeyError(f"Invalid key '{key}'. Allowed keys: {list(self._defaults.keys())}.")
try:
validated_value = validate_value(resolved_key, value)
setattr(self, resolved_key, validated_value)
except (TypeError, ValueError) as e:
raise ValueError(f"Error updating '{key}': {e}")
# Ensure that no parameter was set multiple times due to synonyms
duplicate_keys = [k for k, v in param_counts.items() if v > 1]
if duplicate_keys:
raise ValueError(f"Duplicate assignment detected for parameters: {duplicate_keys}. "
"Use only one synonym per parameter.")
return self # to enable chaining
# Basic tool for debugging
# --------------------------------------------------------------------
# STRUCT method - returns the equivalent dictionary from an object
# --------------------------------------------------------------------
def struct(self):
""" returns the equivalent dictionary from an object """
return dict((key, getattr(self, key)) for key in dir(self) if key not in dir(self.__class__))
# %% Mesh class
# Mesh class
# =======================
class mesh():
""" simple nodes class for finite-volume methods """
def __init__(self,l,n,x0=0,index=None):
self.x0 = x0
self.l = l
self.n = n
de = dw = l/(2*n)
self.de = np.ones(n)*de
self.dw = np.ones(n)*dw
self.xmesh = np.linspace(0+dw,l-de,n) # nodes positions
self.w = self.xmesh - dw
self.e = self.xmesh + de
self.index = np.full(n, int(index), dtype=np.int32)
def __repr__(self):
print(f"-- mesh object (layer index={self.index[0]}) --")
print("%25s = %0.4g" % ("start at x0", self.x0))
print("%25s = %0.4g" % ("domain length l", self.l))
print("%25s = %0.4g" % ("number of nodes n", self.n))
print("%25s = %0.4g" % ("dw", self.dw[0]))
print("%25s = %0.4g" % ("de", self.de[0]))
return "mesh%d=[%0.4g %0.4g]" % \
(self.n,self.x0+self.xmesh[0],self.x0+self.xmesh[-1])
# %% Material classes
"""
=======================================================
Child classes derived from layer
this section can be extended to define specific layers
* polymer
* ink
* air
* paper and board
These classes are more flexible than the parent class layer.
They can include temperature dependence, refined tunning, etc.
Properties taken from
* linear thermal expansoin
https://omnexus.specialchem.com/polymer-properties/properties/coefficient-of-linear-thermal-expansion
Once the layers are incorporated in a multilayer structure,
they loose their original subclass and become only an object
layer. These subclasses are therefore useful to refine the
properties of each layer before standarizing them.
Polarity index is used as an helper to set Henri-like coefficients.
A common scale for polarity index for solvents is from 0 to 10:
- 0-3: Non-polar solvents (e.g., hexane)
- 4-6: Moderately polar solvents (e.g., acetone)
- 7-10: Polar solvents (e.g., water)
We consider that polymers are solid solvents.
=========================================================
"""
# <<<<<<<<<<<<<<<<<<<<<<< P O L Y O L E F I N S >>>>>>>>>>>>>>>>>>>>>>
# <-- LDPE polymer ---------------------------------->
class LDPE(layer):
""" extended pantankar.layer for low-density polyethylene LDPE """
_chemicalsubstance = "ethylene" # monomer for polymers
_polarityindex = 1.0 # Very non-polar (typical for polyolefins)
def __init__(self,l=100e-6,D=1e-12,T=None,
k=None,C0=None,lunit=None,Dunit=None,kunit=None,Cunit=None,
layername="layer in LDPE",**extra):
""" LDPE layer constructor """
super().__init__(
l=l,D=D,k=k,C0=C0, T=T,
lunit=lunit,Dunit=Dunit,kunit=kunit,Cunit=Cunit,
layername=layername,
layertype="polymer", # set by default at inititialization
layermaterial="low-density polyethylene",
layercode="LDPE",
**extra
)
def density(self,T=None):
""" density of LDPE: density(T in K) """
T = self.T if T is None else check_units(T,None,"degC")[0]
return 920 *(1-3*(T-layer._defaults["Td"])*20e-5),"kg/m**3" # lowest temperature
@property
def Tg(self):
""" glass transition temperature of LDPE """
return -130,"degC" # lowest temperature
# <-- HDPE polymer ---------------------------------->
class HDPE(layer):
""" extended pantankar.layer for high-density polyethylene HDPE """
_chemicalsubstance = "ethylene" # monomer for polymers
_polarityindex = 2.0 # Non-polar, slightly higher density, similar overall polarity to LDPE
def __init__(self,l=500e-6,D=1e-13, T=None,
k=None,C0=None,lunit=None,Dunit=None,kunit=None,Cunit=None,
layername="layer in HDPE",**extra):
""" HDPE layer constructor """
layer.__init__(self,
l=l,D=D,k=k,C0=C0, T=T,
lunit=lunit,Dunit=Dunit,kunit=kunit,Cunit=Cunit,
layername=layername,
layertype="polymer", # set by default at inititialization
layermaterial="high-density polyethylene",
layercode="HDPE",
**extra
)
def density(self,T=None):
""" density of HDPE: density(T in K) """
T = self.T if T is None else check_units(T,None,"degC")[0]
return 940 *(1-3*(T-layer._defaults["Td"])*11e-5),"kg/m**3" # lowest temperature
@property
def Tg(self):
""" glass transition temperature of HDPE """
return -100,"degC" # highest temperature
# <-- LLDPE polymer ---------------------------------->
class LLDPE(layer):
""" extended pantankar.layer for linear low-density polyethylene LLDPE """
_chemicalsubstance = "ethylene" # monomer for polymers
_polarityindex = 1.5 # Similar to LDPE, can be slightly more polar if co-monomer is present
def __init__(self, l=80e-6, D=1e-12, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in LLDPE",**extra):
"""
LLDPE layer constructor
Defaults are set to typical values found in the literature or between
LDPE/HDPE ones. Adjust them as necessary for your models.
"""
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="linear low-density polyethylene",
layercode="LLDPE",
**extra
)
def density(self, T=None):
"""
density of LLDPE: density(T in K)
By default, uses an approximate value between LDPE and HDPE.
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
# Similar formula to LDPE and HDPE, with a coefficient suitable for LLDPE.
return 915 * (1 - 3 * (T - layer._defaults["Td"]) * 15e-5), "kg/m**3"
@property
def Tg(self):
"""
glass transition temperature of LLDPE
Typically close to LDPE, though slightly higher or lower can be found in the literature.
"""
return -120, "degC"
# <-- PP polymer ---------------------------------->
class PP(layer):
_chemicalsubstance = "propylene" # monomer for polymers
_polarityindex = 1.0 # Among the least polar, similar to PE
""" extended pantankar.layer for isotactic polypropylene PP """
def __init__(self,l=300e-6,D=1e-14, T=None,
k=None,C0=None,lunit=None,Dunit=None,kunit=None,Cunit=None,
layername="layer in PP",**extra):
""" PP layer constructor """
layer.__init__(self,
l=l,D=D,k=k,C0=C0, T=T,
lunit=lunit,Dunit=Dunit,kunit=kunit,Cunit=Cunit,
layername=layername,
layertype="polymer", # set by default at inititialization
layermaterial="isotactic polypropylene",
layercode="PP",
**extra
)
def density(self,T=None):
""" density of PP: density(T in K) """
T = self.T if T is None else check_units(T,None,"degC")[0]
return 910 *(1-3*(T-layer._defaults["Td"])*7e-5),"kg/m**3" # lowest temperature
@property
def Tg(self):
""" glass transition temperature of PP """
return 0,"degC" # highest temperature
# -- PPrubber (atactic polypropylene) ---------------------------------
class PPrubber(layer):
_chemicalsubstance = "propylene" # monomer for polymers
_polarityindex = 1.0 # Also very non-polar
""" extended pantankar.layer for atactic (rubbery) polypropylene PP """
def __init__(self, l=100e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in PPrubber",**extra):
""" PPrubber layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="atactic polypropylene",
layercode="aPP",
**extra
)
def density(self, T=None):
"""
density of atactic (rubbery) PP: density(T in K)
Approximate initial density ~900 kg/m^3, linear thermal expansion factor
can be adjusted.
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 900 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of atactic/rubbery PP """
return -20, "degC"
# -- oPP (bioriented polypropylene) ------------------------------------
class oPP(layer):
""" extended pantankar.layer for bioriented polypropylene oPP """
_chemicalsubstance = "propylene" # monomer for polymers
_polarityindex = 1.0 # Non-polar, but oriented film might have slight morphological differences
def __init__(self, l=40e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in oPP",**extra):
""" oPP layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="bioriented polypropylene",
layercode="oPP",
**extra
)
def density(self, T=None):
"""
density of bioriented PP: density(T in K)
Typically close to isotactic PP around ~910 kg/m^3.
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 910 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of bioriented PP """
return 0, "degC"
# <<<<<<<<<<<<<<<<<<<<<<< P O L Y V I N Y L S >>>>>>>>>>>>>>>>>>>>>>
# -- PS (polystyrene) -----------------------------------------------
class PS(layer):
""" extended pantankar.layer for polystyrene (PS) """
_chemicalsubstance = "styrene" # monomer for polymers
_polarityindex = 3.0 # Slightly more polar than polyolefins, but still considered relatively non-polar
def __init__(self, l=100e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in PS",**extra):
""" PS layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="polystyrene",
layercode="PS",
**extra
)
def density(self, T=None):
"""
density of PS: ~1050 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1050 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of PS """
return 100, "degC"
# -- HIPS (high-impact polystyrene) -----------------------------------
class HIPS(layer):
""" extended pantankar.layer for high-impact polystyrene (HIPS) """
_chemicalsubstance = "styrene" # monomer for polymers
_polarityindex = 3.0 # Similar or very close to PS in polarity
def __init__(self, l=100e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in HIPS",**extra):
""" HIPS layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="high-impact polystyrene",
layercode="HIPS",
**extra
)
def density(self, T=None):
"""
density of HIPS: ~1040 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1040 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of HIPS """
return 95, "degC"
# -- PBS (assuming a styrene-based polymer) ---------------------------
class SBS(layer):
_chemicalsubstance = "styrene" # Styrene + butadiene
_polarityindex = 3.5 # Non-polar but somewhat more interactive than pure PE/PP due to styrene units
"""
extended pantankar.layer for a styrene-based SBS
Adjust Tg/density as needed for your scenario.
"""
def __init__(self, l=100e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in PBS",**extra):
""" DBS layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="styrene-based polymer SBS",
layercode="SBS",
**extra
)
def density(self, T=None):
"""
density of 'DBS': approximate, around ~1030 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1030 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of 'DBS' """
return 90, "degC"
# -- rigidPVC ---------------------------------------------------------
class rigidPVC(layer):
""" extended pantankar.layer for rigid PVC """
_chemicalsubstance = "vinyl chloride" # monomer for polymers
_polarityindex = 4.0 # Chlorine substituents give moderate polarity.
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in rigid PVC",**extra):
""" rigid PVC layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="rigid PVC",
layercode="PVC",
**extra
)
def density(self, T=None):
"""
density of rigid PVC: ~1400 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1400 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of rigid PVC """
return 80, "degC"
# -- plasticizedPVC ---------------------------------------------------
class plasticizedPVC(layer):
""" extended pantankar.layer for plasticized PVC """
_chemicalsubstance = "vinyl chloride" # monomer for polymers
_polarityindex = 4.5 # Plasticizers can slightly change overall polarity/solubility.
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in plasticized PVC",**extra):
""" plasticized PVC layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="plasticized PVC",
layercode="pPVC",
**extra
)
def density(self, T=None):
"""
density of plasticized PVC: ~1300 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1300 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of plasticized PVC """
return -40, "degC"
# <<<<<<<<<<<<<<<<<<<<<<< P O L Y E S T E R S >>>>>>>>>>>>>>>>>>>>>>
# -- gPET (glassy PET, T < 76°C) --------------------------------------
class gPET(layer):
""" extended pantankar.layer for PET in its glassy state (below ~76°C) """
_chemicalsubstance = "ethylene terephthalate" # monomer for polymers
_polarityindex = 5.0 # Polyester with significant dipolar interactions (Ph = phenylene ring).
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in gPET",**extra):
""" glassy PET layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="glassy PET",
layercode="PET",
**extra
)
def density(self, T=None):
"""
density of glassy PET: ~1350 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1350 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate glass transition temperature of PET """
return 76, "degC"
# -- rPET (rubbery PET, T > 76°C) --------------------------------------
class rPET(layer):
""" extended pantankar.layer for PET in its rubbery state (above ~76°C) """
_chemicalsubstance = "ethylene terephthalate" # monomer for polymers
_polarityindex = 5.0 # Polyester with significant dipolar interactions (Ph = phenylene ring).
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in rPET",**extra):
""" rubbery PET layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="rubbery PET",
layercode="rPET",
**extra
)
def density(self, T=None):
"""
density of rubbery PET: ~1350 kg/m^3
but with a different expansion slope possible, if needed
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1350 * (1 - 3*(T - layer._defaults["Td"]) * 1e-4), "kg/m**3"
@property
def Tg(self):
""" approximate glass transition temperature of PET """
return 76, "degC"
# -- PBT --------------------------------------------------------------
class PBT(layer):
""" extended pantankar.layer for polybutylene terephthalate (PBT) """
_chemicalsubstance = "Buthylene terephthalate" # monomer for polymers
_polarityindex = 5.5 # Similar to PET, slight structural differences
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in PBT",**extra):
""" PBT layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="polybutylene terephthalate",
layercode="PBT",
**extra
)
def density(self, T=None):
"""
density of PBT: ~1310 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1310 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of PBT """
return 40, "degC"
# -- PEN --------------------------------------------------------------
class PEN(layer):
_chemicalsubstance = "Buthylene terephthalate" # monomer for polymers
_polarityindex = 6 # More aromatic than PET, often better barrier properties
""" extended pantankar.layer for polyethylene naphthalate (PEN) """
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in PEN",**extra):
""" PEN layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="polyethylene naphthalate",
layercode="PEN",
**extra
)
def density(self, T=None):
"""
density of PEN: ~1330 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1330 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of PEN """
return 120, "degC"
# <<<<<<<<<<<<<<<<<<<<<<< P O L Y A M I D E S >>>>>>>>>>>>>>>>>>>>>>
# -- PA6 --------------------------------------------------------------
class PA6(layer):
_chemicalsubstance = "caprolactam" # monomer for polymers
_polarityindex = 7.5 # Strong hydrogen-bonding, thus quite polar.
""" extended pantankar.layer for polyamide 6 (PA6) """
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in PA6",**extra):
""" PA6 layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="polyamide 6",
layercode="PA6",
**extra
)
def density(self, T=None):
"""
density of PA6: ~1140 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1140 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of PA6 """
return 50, "degC"
# -- PA66 -------------------------------------------------------------
class PA66(layer):
_chemicalsubstance = "hexamethylenediamine" # monomer for polymers
_polarityindex = 7.5 # Similar to PA6, strongly polar with hydrogen bonds.
""" extended pantankar.layer for polyamide 66 (PA66) """
def __init__(self, l=200e-6, D=1e-14, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="layer in PA66",**extra):
""" PA66 layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="polymer",
layermaterial="polyamide 6,6",
layercode="PA6,6",
**extra
)
def density(self, T=None):
"""
density of PA66: ~1150 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1150 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
@property
def Tg(self):
""" glass transition temperature of PA66 """
return 70, "degC"
# <<<<<<<<<<<<<<<<<<<<<<< A D H E S I V E S >>>>>>>>>>>>>>>>>>>>>>
# -- AdhesiveNaturalRubber --------------------------------------------
class AdhesiveNaturalRubber(layer):
_chemicalsubstance = "cis-1,4-polyisoprene" # monomer for polymers
_polarityindex = 2 # Mostly non-polar; elasticity from cis-isoprene chains.
""" extended pantankar.layer for natural rubber adhesives """
def __init__(self, l=20e-6, D=1e-13, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="adhesive natural rubber",**extra):
""" constructor for a natural rubber-based adhesive layer """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="adhesive",
layermaterial="natural rubber adhesive",
layercode="rubber",
**extra
)
def density(self, T=None):
""" typical density ~910 kg/m^3, adjust as needed """
T = self.T if T is None else check_units(T, None, "degC")[0]
return 910 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate Tg of natural rubber adhesives """
return -70, "degC"
# -- AdhesiveSyntheticRubber ------------------------------------------
class AdhesiveSyntheticRubber(layer):
_chemicalsubstance = "cis-1,4-polyisoprene" # styrene-butadiene rubber (SBR) or similar
_polarityindex = 2.0 # non-polar or slightly polar, depending on rubber type.
""" extended pantankar.layer for synthetic rubber adhesives """
def __init__(self, l=20e-6, D=1e-13, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="adhesive synthetic rubber",**extra):
""" constructor for a synthetic rubber-based adhesive layer """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="adhesive",
layermaterial="synthetic rubber adhesive",
layercode="sRubber",
**extra
)
def density(self, T=None):
""" typical density ~920 kg/m^3, adjust as needed """
T = self.T if T is None else check_units(T, None, "degC")[0]
return 920 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate Tg of synthetic rubber adhesives """
return -50, "degC"
# -- AdhesiveEVA (ethylene-vinyl acetate) ------------------------------
class AdhesiveEVA(layer):
_chemicalsubstance = "ethylene" # Ethylene + vinyl acetate
_polarityindex = 2.5 # Mostly non-polar backbone with some polar acetate groups.
""" extended pantankar.layer for EVA-based adhesives """
def __init__(self, l=20e-6, D=1e-13, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="adhesive EVA",**extra):
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="adhesive",
layermaterial="EVA adhesive",
layercode="EVA",
**extra
)
def density(self, T=None):
""" typical density ~930 kg/m^3 """
T = self.T if T is None else check_units(T, None, "degC")[0]
return 930 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate Tg of EVA adhesives """
return -30, "degC"
# -- AdhesiveVAE (vinyl acetate-ethylene) -----------------------------
class AdhesiveVAE(layer):
_chemicalsubstance = "vinyl acetate" # Ethylene + vinyl acetate
_polarityindex = 4.0 # More polar than EVA (larger fraction of acetate).
""" extended pantankar.layer for VAE adhesives """
def __init__(self, l=20e-6, D=1e-13, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="adhesive VAE",**extra):
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="adhesive",
layermaterial="VAE adhesive",
layercode="VAE",
**extra
)
def density(self, T=None):
""" typical density ~950 kg/m^3 """
T = self.T if T is None else check_units(T, None, "degC")[0]
return 950 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate Tg of VAE adhesives """
return 10, "degC"
# -- AdhesivePVAC (polyvinyl acetate) ---------------------------------
class AdhesivePVAC(layer):
""" extended pantankar.layer for PVAc adhesives """
_chemicalsubstance = "vinyl acetate" # Vinyl acetate (CH₂=CHO–Ac)
_polarityindex = 7.0 # PVAc is fairly polar (acetate groups)
def __init__(self, l=20e-6, D=1e-13, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="adhesive PVAc",**extra):
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="adhesive",
layermaterial="PVAc adhesive",
layercode="PVAc",
**extra
)
def density(self, T=None):
""" typical density ~1100 kg/m^3 """
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1100 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate Tg of PVAc adhesives """
return 35, "degC"
# -- AdhesiveAcrylate -------------------------------------------------
class AdhesiveAcrylate(layer):
""" extended pantankar.layer for acrylate adhesives """
_chemicalsubstance = "n-butyl acrylate" # Acrylic esters (e.g. n-butyl acrylate)
_polarityindex = 6.0 # Ester groups confer moderate polarity
def __init__(self, l=20e-6, D=1e-13, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="adhesive acrylate",**extra):
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="adhesive",
layermaterial="acrylate adhesive",
layercode="Acryl",
**extra
)
def density(self, T=None):
""" typical density ~1000 kg/m^3 """
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1000 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate Tg of acrylate adhesives """
return -20, "degC"
# -- AdhesivePU (polyurethane) ----------------------------------------
class AdhesivePU(layer):
""" extended pantankar.layer for polyurethane adhesives """
_chemicalsubstance = "diisocyanate" # Diisocyanate + polyol (–NH–CO–O–)
_polarityindex = 5.0 # Can vary widely by chemistry; moderate polarity.
def __init__(self, l=20e-6, D=1e-13, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="adhesive PU",**extra):
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="adhesive",
layermaterial="polyurethane adhesive",
layercode="PU",
**extra
)
def density(self, T=None):
""" typical density ~1100 kg/m^3 """
T = self.T if T is None else check_units(T, None, "degC")[0]
return 1100 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
@property
def Tg(self):
""" approximate Tg of polyurethane adhesives """
return -50, "degC"
# <<<<<<<<<<<<<<<<<<<<<<< P A P E R & C A R D B O A R D >>>>>>>>>>>>>>>>>>>>>>
# -- Paper ------------------------------------------------------------
class Paper(layer):
""" extended pantankar.layer for paper (cellulose-based) """
_physicalstate = "porous" # solid (default), liquid, gas, porous
_chemicalclass = "other" # polymer (default), other
_chemicalsubstance = "cellulose" # Cellulose (β-D-glucopyranose units)
_polarityindex = 8.5 # Highly polar, strong hydrogen-bonding.
def __init__(self, l=80e-6, D=1e-15, T=None, # a guess for barrier properties
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="paper layer",**extra):
""" Paper layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="paper",
layermaterial="paper",
layercode="paper",
**extra
)
def density(self, T=None):
"""
approximate density for typical paper ~800 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 800 * (1 - 3*(T - layer._defaults["Td"]) * 1e-5), "kg/m**3"
@property
def Tg(self):
"""
glass transition temperature is not typically used for paper,
but we provide a placeholder.
"""
return 200, "degC" # purely illustrative placeholder
# -- Cardboard --------------------------------------------------------
class Cardboard(layer):
""" extended pantankar.layer for cardboard (cellulose-based) """
_physicalstate = "porous" # solid (default), liquid, gas, porous
_chemicalclass = "other" # polymer (default), other
_chemicalsubstance = "cellulose"
_polarityindex = 8.0 # Can vary widely by chemistry; moderate polarity.
def __init__(self, l=500e-6, D=1e-15, T=None,
k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None,
layername="cardboard layer",**extra):
""" Cardboard layer constructor """
super().__init__(
l=l, D=D, k=k, C0=C0, T=T,
lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit,
layername=layername,
layertype="paper",
layermaterial="cardboard",
layercode="board",
**extra
)
def density(self, T=None):
"""
approximate density for typical cardboard ~700 kg/m^3
"""
T = self.T if T is None else check_units(T, None, "degC")[0]
return 700 * (1 - 3*(T - layer._defaults["Td"]) * 1e-5), "kg/m**3"
@property
def Tg(self):
"""
same placeholder concept for paper-based material
"""
return 200, "degC"
# <<<<<<<<<<<<<<<<<<<<<<< G A S E S >>>>>>>>>>>>>>>>>>>>>>
# <-- air | ideal gas layer ---------------------------------->
class air(layer):
""" extended pantankar.layer for ideal gases such as air """
_physicalstate = "gas" # solid (default), liquid, gas, porous
_chemicalclass = "other" # polymer (default), other
def __init__(self,l=1e-2,D=1e-6,T=None,
lunit=None,Dunit=None,Cunit=None,
layername="air layer",layercode="air",**extra):
""" air layer constructor """
T = layer._defaults["T"] if T is None else check_units(T,None,"degC")[0]
TK = constants["T0K"]+T
kair = 1/(constants["R"] *TK)
kairunit = constants["iRT0Kunit"]
layer.__init__(self,
l=l,D=D,k=kair,C0=0,T=T,
lunit=lunit,Dunit=Dunit,kunit=kairunit,Cunit=Cunit,
layername=layername,
layertype="air", # set by default at inititialization
layermaterial="ideal gas",
layercode="gas",
**extra
)
def density(self, T=None):
"""Density of air at atmospheric pressure: density(T in K)"""
TK = self.TK if T is None else check_units(T,None,"K")[0]
P_atm = 101325 # Pa (1 atm)
M_air = 28.9647 # g/mol = 0.0289647 kg/mol (Molar mass of dry air).
return P_atm / ((constants["R"]/M_air) * TK), "kg/m**3"
# %% For testing and debugging
# ===================================================
# main()
# ===================================================
# for debugging purposes (code called as a script)
# the code is called from here
# ===================================================
if __name__ == '__main__':
G = air(T=60)
P = LDPE(D=1e-8,Dunit='cm**2/s')
P = LDPE(D=(1e-8,"cm**2/s"))
A = LDPE()
A=layer(D=1e-14,l=50e-6)
print("\n",repr(A),"\n"*2)
A
B=A*3
D = B[1:2]
B=A+A
C=B[1]
B.l = [1,2]
A.struct()
E = B*4
#E[1:4]=[]
E
# ======
A = layer(layername = "layer A")
B = layer(layername = "layer B")
C = layer(layername = "layer C")
D = layer(layername = "layer D")
# test = A+B+C+D
# test[2] = test[0]
# test[3] = []
# test
test = A+A+B+B+B+C
print("\n",repr(test),"\n"*2)
testsimple = test.simplify()
print("\n",repr(testsimple),"\n"*2)
testsimple.mesh()
# test with substance
m1 = migrant(name='limonene')
m2 = migrant(name='anisole')
pet_with_limonene = gPET(substance=m1,D=None,T=40,l=(50,"um"))
PP_with_anisole = PP(substance=m2,D=None,T=40,l=(200,"um"))
print("\n",repr(pet_with_limonene),"\n"*2)
test = pet_with_limonene + PP_with_anisole
test.D
print("\n",repr(test),"\n"*2)
Functions
def check_units(value, ProvidedUnits=None, ExpectedUnits=None, defaulttempUnits='degC')
-
check numeric inputs and convert them to SI units
Expand source code
def check_units(value,ProvidedUnits=None,ExpectedUnits=None,defaulttempUnits="degC"): """ check numeric inputs and convert them to SI units """ # by convention, NumPy arrays and None are return unchanged (prevent nesting) if isinstance(value,np.ndarray) or value is None: return value,UnknownUnits if isinstance(value,tuple): if len(value) != 2: raise ValueError('value should be a tuple: (value,"unit"') ProvidedUnits = value[1] value = value[0] if isinstance(value,list): # the function is vectorized value = np.array(value) if {"degC", "K"} & {ProvidedUnits, ExpectedUnits}: # the value is a temperature ExpectedUnits = defaulttempUnits if ExpectedUnits is None else ExpectedUnits ProvidedUnits = ExpectedUnits if ProvidedUnits is None else ProvidedUnits if ProvidedUnits=="degC" and ExpectedUnits=="K": value += constants["T0K"] elif ProvidedUnits=="K" and ExpectedUnits=="degC": value -= constants["T0K"] return np.array([value]),ExpectedUnits else: # the value is not a temperature ExpectedUnits = NoUnits if ExpectedUnits is None else ExpectedUnits if (ProvidedUnits==ExpectedUnits) or (ProvidedUnits==NoUnits) or (ExpectedUnits==None): conversion =1 # no conversion needed units = ExpectedUnits if ExpectedUnits is not None else NoUnits else: q0,conversion,units = toSI(qSI(1,ProvidedUnits)) return np.array([value*conversion]),units
def fixSIbase(registry)
-
Set the application registry, which is used for unpickling operations and when invoking pint.Quantity or pint.Unit directly.
Parameters
registry
:pint.UnitRegistry
Expand source code
def set_application_registry(registry): """Set the application registry, which is used for unpickling operations and when invoking pint.Quantity or pint.Unit directly. Parameters ---------- registry : pint.UnitRegistry """ application_registry.set(registry)
def format_scientific_latex(value, numdigits=4, units=None, prefix='', mathmode='$')
-
Formats a number in scientific notation only when necessary, using LaTeX.
Parameters:
value : float The number to format. numdigits : int, optional (default=4) Number of significant digits for formatting. units : str, optional (default=None) LaTeX representation of units. If None, no units are added. prefix: str, optional (default="") mathmode: str, optional (default="$")
Returns:
str The formatted number in standard or LaTeX scientific notation.
Examples:
>>> format_scientific_latex(1e-12) '$10^{-12}$'
>>> format_scientific_latex(1.5e-3) '0.0015'
>>> format_scientific_latex(1.3e10) '$1.3 \cdot 10^{10}$'
>>> format_scientific_latex(0.00341) '0.00341'
>>> format_scientific_latex(3.41e-6) '$3.41 \cdot 10^{-6}$'
Expand source code
def format_scientific_latex(value, numdigits=4, units=None, prefix="",mathmode="$"): """ Formats a number in scientific notation only when necessary, using LaTeX. Parameters: ----------- value : float The number to format. numdigits : int, optional (default=4) Number of significant digits for formatting. units : str, optional (default=None) LaTeX representation of units. If None, no units are added. prefix: str, optional (default="") mathmode: str, optional (default="$") Returns: -------- str The formatted number in standard or LaTeX scientific notation. Examples: --------- >>> format_scientific_latex(1e-12) '$10^{-12}$' >>> format_scientific_latex(1.5e-3) '0.0015' >>> format_scientific_latex(1.3e10) '$1.3 \\cdot 10^{10}$' >>> format_scientific_latex(0.00341) '0.00341' >>> format_scientific_latex(3.41e-6) '$3.41 \\cdot 10^{-6}$' """ if value == 0: return "$0$" if units is None else rf"$0 \, {units}$" # Get formatted number using Matlab-like %g behavior formatted = f"{value:.{numdigits}g}" # If the formatting results in an `e` notation, convert to LaTeX if "e" in formatted or "E" in formatted: coefficient, exponent = formatted.split("e") exponent = int(exponent) # Convert exponent to integer # Remove trailing zeros in coefficient coefficient = coefficient.rstrip("0").rstrip(".") # Ensures "1.00" -> "1" # LaTeX scientific format sci_notation = rf"{prefix}{coefficient} \cdot 10^{{{exponent}}}" return sci_notation if units is None else rf"{mathmode}{sci_notation} \, {units}{mathmode}" # Otherwise, return standard notation return formatted if units is None else rf"{mathmode}{prefix}{formatted} \, {units}{mathmode}"
def help_layer()
-
Print all subclasses with their type/material info in a Markdown table with dynamic column widths.
Expand source code
def help_layer(): """ Print all subclasses with their type/material info in a Markdown table with dynamic column widths. """ derived = list_layer_subclasses() # Extract table content headers = ["Class Name", "Type", "Material", "Code"] rows = [[item["classname"], item["type"], item["material"], item["code"]] for item in derived] # Compute column widths based on content col_widths = [max(len(str(cell)) for cell in col) for col in zip(headers, *rows)] # Formatting row template row_format = "| " + " | ".join(f"{{:<{w}}}" for w in col_widths) + " |" # Print header print(row_format.format(*headers)) print("|-" + "-|-".join("-" * w for w in col_widths) + "-|") # Print table rows for row in rows: print(row_format.format(*row))
def list_layer_subclasses()
-
Lists all classes in this module that derive from 'layer', along with their layertype and layermaterial properties.
Returns
list of tuples (classname, layertype, layermaterial)
Expand source code
def list_layer_subclasses(): """ Lists all classes in this module that derive from 'layer', along with their layertype and layermaterial properties. Returns: list of tuples (classname, layertype, layermaterial) """ subclasses_info = [] current_module = sys.modules[__name__] # This refers to layer.py itself for name, obj in inspect.getmembers(current_module, inspect.isclass): # Make sure 'obj' is actually a subclass of layer (and not 'layer' itself) if obj is not layer and issubclass(obj, layer): try: # Instantiate with default parameters so that .layertype / .layermaterial are accessible instance = obj() subclasses_info.append( {"classname":name, "type":instance._type[0], "material":instance._material[0], "code":instance._code[0]} ) except TypeError as e: # Log error and rethrow for debugging print(f"⚠️ Error: Could not instantiate class '{name}'. Check its constructor.") print(f"🔍 Exception: {e}") raise # Rethrow the error with full traceback return subclasses_info
def toSI(q)
-
Expand source code
def toSI(q): q=q.to_base_units(); return q,q.m,str(q.u)
Classes
class AdhesiveAcrylate (l=2e-05, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='adhesive acrylate', **extra)
-
extended pantankar.layer for acrylate adhesives
Parameters
layername
:TYPE
, optional, string
- DESCRIPTION. Layer Name. The default is "my layer".
layertype
:TYPE
, optional, string
- DESCRIPTION. Layer Type. The default is "unknown type".
layermaterial
:TYPE
, optional, string
- DESCRIPTION. Material identification . The default is "unknown material".
- PHYSICAL QUANTITIES
l
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Thickness. The default is 50e-6 (m).
D
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s).
k
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.).
C0
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Initial concentration. The default is 1000 (a.u.).
- PHYSICAL UNITS
lunit
:TYPE
, optional, string
- DESCRIPTION. Length units. The default unit is "m.
Dunit
:TYPE
, optional, string
- DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s".
kunit
:TYPE
, optional, string
- DESCRIPTION. Henry-like coefficient. The default unit is "a.u.".
Cunit
:TYPE
, optional, string
- DESCRIPTION. Initial concentration. The default unit is "a.u.".
Returns
a monolayer object which can be assembled into a multilayer structure
Expand source code
class AdhesiveAcrylate(layer): """ extended pantankar.layer for acrylate adhesives """ _chemicalsubstance = "n-butyl acrylate" # Acrylic esters (e.g. n-butyl acrylate) _polarityindex = 6.0 # Ester groups confer moderate polarity def __init__(self, l=20e-6, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="adhesive acrylate",**extra): super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="adhesive", layermaterial="acrylate adhesive", layercode="Acryl", **extra ) def density(self, T=None): """ typical density ~1000 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1000 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate Tg of acrylate adhesives """ return -20, "degC"
Ancestors
Instance variables
var Tg
-
approximate Tg of acrylate adhesives
Expand source code
@property def Tg(self): """ approximate Tg of acrylate adhesives """ return -20, "degC"
Methods
def density(self, T=None)
-
typical density ~1000 kg/m^3
Expand source code
def density(self, T=None): """ typical density ~1000 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1000 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class AdhesiveEVA (l=2e-05, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='adhesive EVA', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
Parameters
layername
:TYPE
, optional, string
- DESCRIPTION. Layer Name. The default is "my layer".
layertype
:TYPE
, optional, string
- DESCRIPTION. Layer Type. The default is "unknown type".
layermaterial
:TYPE
, optional, string
- DESCRIPTION. Material identification . The default is "unknown material".
- PHYSICAL QUANTITIES
l
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Thickness. The default is 50e-6 (m).
D
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s).
k
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.).
C0
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Initial concentration. The default is 1000 (a.u.).
- PHYSICAL UNITS
lunit
:TYPE
, optional, string
- DESCRIPTION. Length units. The default unit is "m.
Dunit
:TYPE
, optional, string
- DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s".
kunit
:TYPE
, optional, string
- DESCRIPTION. Henry-like coefficient. The default unit is "a.u.".
Cunit
:TYPE
, optional, string
- DESCRIPTION. Initial concentration. The default unit is "a.u.".
Returns
a monolayer object which can be assembled into a multilayer structure
Expand source code
class AdhesiveEVA(layer): _chemicalsubstance = "ethylene" # Ethylene + vinyl acetate _polarityindex = 2.5 # Mostly non-polar backbone with some polar acetate groups. """ extended pantankar.layer for EVA-based adhesives """ def __init__(self, l=20e-6, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="adhesive EVA",**extra): super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="adhesive", layermaterial="EVA adhesive", layercode="EVA", **extra ) def density(self, T=None): """ typical density ~930 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 930 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate Tg of EVA adhesives """ return -30, "degC"
Ancestors
Instance variables
var Tg
-
approximate Tg of EVA adhesives
Expand source code
@property def Tg(self): """ approximate Tg of EVA adhesives """ return -30, "degC"
Methods
def density(self, T=None)
-
typical density ~930 kg/m^3
Expand source code
def density(self, T=None): """ typical density ~930 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 930 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class AdhesiveNaturalRubber (l=2e-05, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='adhesive natural rubber', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
constructor for a natural rubber-based adhesive layer
Expand source code
class AdhesiveNaturalRubber(layer): _chemicalsubstance = "cis-1,4-polyisoprene" # monomer for polymers _polarityindex = 2 # Mostly non-polar; elasticity from cis-isoprene chains. """ extended pantankar.layer for natural rubber adhesives """ def __init__(self, l=20e-6, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="adhesive natural rubber",**extra): """ constructor for a natural rubber-based adhesive layer """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="adhesive", layermaterial="natural rubber adhesive", layercode="rubber", **extra ) def density(self, T=None): """ typical density ~910 kg/m^3, adjust as needed """ T = self.T if T is None else check_units(T, None, "degC")[0] return 910 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate Tg of natural rubber adhesives """ return -70, "degC"
Ancestors
Instance variables
var Tg
-
approximate Tg of natural rubber adhesives
Expand source code
@property def Tg(self): """ approximate Tg of natural rubber adhesives """ return -70, "degC"
Methods
def density(self, T=None)
-
typical density ~910 kg/m^3, adjust as needed
Expand source code
def density(self, T=None): """ typical density ~910 kg/m^3, adjust as needed """ T = self.T if T is None else check_units(T, None, "degC")[0] return 910 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class AdhesivePU (l=2e-05, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='adhesive PU', **extra)
-
extended pantankar.layer for polyurethane adhesives
Parameters
layername
:TYPE
, optional, string
- DESCRIPTION. Layer Name. The default is "my layer".
layertype
:TYPE
, optional, string
- DESCRIPTION. Layer Type. The default is "unknown type".
layermaterial
:TYPE
, optional, string
- DESCRIPTION. Material identification . The default is "unknown material".
- PHYSICAL QUANTITIES
l
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Thickness. The default is 50e-6 (m).
D
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s).
k
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.).
C0
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Initial concentration. The default is 1000 (a.u.).
- PHYSICAL UNITS
lunit
:TYPE
, optional, string
- DESCRIPTION. Length units. The default unit is "m.
Dunit
:TYPE
, optional, string
- DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s".
kunit
:TYPE
, optional, string
- DESCRIPTION. Henry-like coefficient. The default unit is "a.u.".
Cunit
:TYPE
, optional, string
- DESCRIPTION. Initial concentration. The default unit is "a.u.".
Returns
a monolayer object which can be assembled into a multilayer structure
Expand source code
class AdhesivePU(layer): """ extended pantankar.layer for polyurethane adhesives """ _chemicalsubstance = "diisocyanate" # Diisocyanate + polyol (–NH–CO–O–) _polarityindex = 5.0 # Can vary widely by chemistry; moderate polarity. def __init__(self, l=20e-6, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="adhesive PU",**extra): super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="adhesive", layermaterial="polyurethane adhesive", layercode="PU", **extra ) def density(self, T=None): """ typical density ~1100 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1100 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate Tg of polyurethane adhesives """ return -50, "degC"
Ancestors
Instance variables
var Tg
-
approximate Tg of polyurethane adhesives
Expand source code
@property def Tg(self): """ approximate Tg of polyurethane adhesives """ return -50, "degC"
Methods
def density(self, T=None)
-
typical density ~1100 kg/m^3
Expand source code
def density(self, T=None): """ typical density ~1100 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1100 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class AdhesivePVAC (l=2e-05, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='adhesive PVAc', **extra)
-
extended pantankar.layer for PVAc adhesives
Parameters
layername
:TYPE
, optional, string
- DESCRIPTION. Layer Name. The default is "my layer".
layertype
:TYPE
, optional, string
- DESCRIPTION. Layer Type. The default is "unknown type".
layermaterial
:TYPE
, optional, string
- DESCRIPTION. Material identification . The default is "unknown material".
- PHYSICAL QUANTITIES
l
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Thickness. The default is 50e-6 (m).
D
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s).
k
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.).
C0
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Initial concentration. The default is 1000 (a.u.).
- PHYSICAL UNITS
lunit
:TYPE
, optional, string
- DESCRIPTION. Length units. The default unit is "m.
Dunit
:TYPE
, optional, string
- DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s".
kunit
:TYPE
, optional, string
- DESCRIPTION. Henry-like coefficient. The default unit is "a.u.".
Cunit
:TYPE
, optional, string
- DESCRIPTION. Initial concentration. The default unit is "a.u.".
Returns
a monolayer object which can be assembled into a multilayer structure
Expand source code
class AdhesivePVAC(layer): """ extended pantankar.layer for PVAc adhesives """ _chemicalsubstance = "vinyl acetate" # Vinyl acetate (CH₂=CHO–Ac) _polarityindex = 7.0 # PVAc is fairly polar (acetate groups) def __init__(self, l=20e-6, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="adhesive PVAc",**extra): super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="adhesive", layermaterial="PVAc adhesive", layercode="PVAc", **extra ) def density(self, T=None): """ typical density ~1100 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1100 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate Tg of PVAc adhesives """ return 35, "degC"
Ancestors
Instance variables
var Tg
-
approximate Tg of PVAc adhesives
Expand source code
@property def Tg(self): """ approximate Tg of PVAc adhesives """ return 35, "degC"
Methods
def density(self, T=None)
-
typical density ~1100 kg/m^3
Expand source code
def density(self, T=None): """ typical density ~1100 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1100 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class AdhesiveSyntheticRubber (l=2e-05, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='adhesive synthetic rubber', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
constructor for a synthetic rubber-based adhesive layer
Expand source code
class AdhesiveSyntheticRubber(layer): _chemicalsubstance = "cis-1,4-polyisoprene" # styrene-butadiene rubber (SBR) or similar _polarityindex = 2.0 # non-polar or slightly polar, depending on rubber type. """ extended pantankar.layer for synthetic rubber adhesives """ def __init__(self, l=20e-6, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="adhesive synthetic rubber",**extra): """ constructor for a synthetic rubber-based adhesive layer """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="adhesive", layermaterial="synthetic rubber adhesive", layercode="sRubber", **extra ) def density(self, T=None): """ typical density ~920 kg/m^3, adjust as needed """ T = self.T if T is None else check_units(T, None, "degC")[0] return 920 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate Tg of synthetic rubber adhesives """ return -50, "degC"
Ancestors
Instance variables
var Tg
-
approximate Tg of synthetic rubber adhesives
Expand source code
@property def Tg(self): """ approximate Tg of synthetic rubber adhesives """ return -50, "degC"
Methods
def density(self, T=None)
-
typical density ~920 kg/m^3, adjust as needed
Expand source code
def density(self, T=None): """ typical density ~920 kg/m^3, adjust as needed """ T = self.T if T is None else check_units(T, None, "degC")[0] return 920 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class AdhesiveVAE (l=2e-05, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='adhesive VAE', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
Parameters
layername
:TYPE
, optional, string
- DESCRIPTION. Layer Name. The default is "my layer".
layertype
:TYPE
, optional, string
- DESCRIPTION. Layer Type. The default is "unknown type".
layermaterial
:TYPE
, optional, string
- DESCRIPTION. Material identification . The default is "unknown material".
- PHYSICAL QUANTITIES
l
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Thickness. The default is 50e-6 (m).
D
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s).
k
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.).
C0
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Initial concentration. The default is 1000 (a.u.).
- PHYSICAL UNITS
lunit
:TYPE
, optional, string
- DESCRIPTION. Length units. The default unit is "m.
Dunit
:TYPE
, optional, string
- DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s".
kunit
:TYPE
, optional, string
- DESCRIPTION. Henry-like coefficient. The default unit is "a.u.".
Cunit
:TYPE
, optional, string
- DESCRIPTION. Initial concentration. The default unit is "a.u.".
Returns
a monolayer object which can be assembled into a multilayer structure
Expand source code
class AdhesiveVAE(layer): _chemicalsubstance = "vinyl acetate" # Ethylene + vinyl acetate _polarityindex = 4.0 # More polar than EVA (larger fraction of acetate). """ extended pantankar.layer for VAE adhesives """ def __init__(self, l=20e-6, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="adhesive VAE",**extra): super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="adhesive", layermaterial="VAE adhesive", layercode="VAE", **extra ) def density(self, T=None): """ typical density ~950 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 950 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate Tg of VAE adhesives """ return 10, "degC"
Ancestors
Instance variables
var Tg
-
approximate Tg of VAE adhesives
Expand source code
@property def Tg(self): """ approximate Tg of VAE adhesives """ return 10, "degC"
Methods
def density(self, T=None)
-
typical density ~950 kg/m^3
Expand source code
def density(self, T=None): """ typical density ~950 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 950 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class Cardboard (l=0.0005, D=1e-15, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='cardboard layer', **extra)
-
extended pantankar.layer for cardboard (cellulose-based)
Cardboard layer constructor
Expand source code
class Cardboard(layer): """ extended pantankar.layer for cardboard (cellulose-based) """ _physicalstate = "porous" # solid (default), liquid, gas, porous _chemicalclass = "other" # polymer (default), other _chemicalsubstance = "cellulose" _polarityindex = 8.0 # Can vary widely by chemistry; moderate polarity. def __init__(self, l=500e-6, D=1e-15, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="cardboard layer",**extra): """ Cardboard layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="paper", layermaterial="cardboard", layercode="board", **extra ) def density(self, T=None): """ approximate density for typical cardboard ~700 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 700 * (1 - 3*(T - layer._defaults["Td"]) * 1e-5), "kg/m**3" @property def Tg(self): """ same placeholder concept for paper-based material """ return 200, "degC"
Ancestors
Instance variables
var Tg
-
same placeholder concept for paper-based material
Expand source code
@property def Tg(self): """ same placeholder concept for paper-based material """ return 200, "degC"
Methods
def density(self, T=None)
-
approximate density for typical cardboard ~700 kg/m^3
Expand source code
def density(self, T=None): """ approximate density for typical cardboard ~700 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 700 * (1 - 3*(T - layer._defaults["Td"]) * 1e-5), "kg/m**3"
Inherited members
class HDPE (l=0.0005, D=1e-13, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in HDPE', **extra)
-
extended pantankar.layer for high-density polyethylene HDPE
HDPE layer constructor
Expand source code
class HDPE(layer): """ extended pantankar.layer for high-density polyethylene HDPE """ _chemicalsubstance = "ethylene" # monomer for polymers _polarityindex = 2.0 # Non-polar, slightly higher density, similar overall polarity to LDPE def __init__(self,l=500e-6,D=1e-13, T=None, k=None,C0=None,lunit=None,Dunit=None,kunit=None,Cunit=None, layername="layer in HDPE",**extra): """ HDPE layer constructor """ layer.__init__(self, l=l,D=D,k=k,C0=C0, T=T, lunit=lunit,Dunit=Dunit,kunit=kunit,Cunit=Cunit, layername=layername, layertype="polymer", # set by default at inititialization layermaterial="high-density polyethylene", layercode="HDPE", **extra ) def density(self,T=None): """ density of HDPE: density(T in K) """ T = self.T if T is None else check_units(T,None,"degC")[0] return 940 *(1-3*(T-layer._defaults["Td"])*11e-5),"kg/m**3" # lowest temperature @property def Tg(self): """ glass transition temperature of HDPE """ return -100,"degC" # highest temperature
Ancestors
Instance variables
var Tg
-
glass transition temperature of HDPE
Expand source code
@property def Tg(self): """ glass transition temperature of HDPE """ return -100,"degC" # highest temperature
Methods
def density(self, T=None)
-
density of HDPE: density(T in K)
Expand source code
def density(self,T=None): """ density of HDPE: density(T in K) """ T = self.T if T is None else check_units(T,None,"degC")[0] return 940 *(1-3*(T-layer._defaults["Td"])*11e-5),"kg/m**3" # lowest temperature
Inherited members
class HIPS (l=0.0001, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in HIPS', **extra)
-
extended pantankar.layer for high-impact polystyrene (HIPS)
HIPS layer constructor
Expand source code
class HIPS(layer): """ extended pantankar.layer for high-impact polystyrene (HIPS) """ _chemicalsubstance = "styrene" # monomer for polymers _polarityindex = 3.0 # Similar or very close to PS in polarity def __init__(self, l=100e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in HIPS",**extra): """ HIPS layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="high-impact polystyrene", layercode="HIPS", **extra ) def density(self, T=None): """ density of HIPS: ~1040 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1040 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of HIPS """ return 95, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of HIPS
Expand source code
@property def Tg(self): """ glass transition temperature of HIPS """ return 95, "degC"
Methods
def density(self, T=None)
-
density of HIPS: ~1040 kg/m^3
Expand source code
def density(self, T=None): """ density of HIPS: ~1040 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1040 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
Inherited members
class LDPE (l=0.0001, D=1e-12, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in LDPE', **extra)
-
extended pantankar.layer for low-density polyethylene LDPE
LDPE layer constructor
Expand source code
class LDPE(layer): """ extended pantankar.layer for low-density polyethylene LDPE """ _chemicalsubstance = "ethylene" # monomer for polymers _polarityindex = 1.0 # Very non-polar (typical for polyolefins) def __init__(self,l=100e-6,D=1e-12,T=None, k=None,C0=None,lunit=None,Dunit=None,kunit=None,Cunit=None, layername="layer in LDPE",**extra): """ LDPE layer constructor """ super().__init__( l=l,D=D,k=k,C0=C0, T=T, lunit=lunit,Dunit=Dunit,kunit=kunit,Cunit=Cunit, layername=layername, layertype="polymer", # set by default at inititialization layermaterial="low-density polyethylene", layercode="LDPE", **extra ) def density(self,T=None): """ density of LDPE: density(T in K) """ T = self.T if T is None else check_units(T,None,"degC")[0] return 920 *(1-3*(T-layer._defaults["Td"])*20e-5),"kg/m**3" # lowest temperature @property def Tg(self): """ glass transition temperature of LDPE """ return -130,"degC" # lowest temperature
Ancestors
Instance variables
var Tg
-
glass transition temperature of LDPE
Expand source code
@property def Tg(self): """ glass transition temperature of LDPE """ return -130,"degC" # lowest temperature
Methods
def density(self, T=None)
-
density of LDPE: density(T in K)
Expand source code
def density(self,T=None): """ density of LDPE: density(T in K) """ T = self.T if T is None else check_units(T,None,"degC")[0] return 920 *(1-3*(T-layer._defaults["Td"])*20e-5),"kg/m**3" # lowest temperature
Inherited members
class LLDPE (l=8e-05, D=1e-12, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in LLDPE', **extra)
-
extended pantankar.layer for linear low-density polyethylene LLDPE
LLDPE layer constructor Defaults are set to typical values found in the literature or between LDPE/HDPE ones. Adjust them as necessary for your models.
Expand source code
class LLDPE(layer): """ extended pantankar.layer for linear low-density polyethylene LLDPE """ _chemicalsubstance = "ethylene" # monomer for polymers _polarityindex = 1.5 # Similar to LDPE, can be slightly more polar if co-monomer is present def __init__(self, l=80e-6, D=1e-12, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in LLDPE",**extra): """ LLDPE layer constructor Defaults are set to typical values found in the literature or between LDPE/HDPE ones. Adjust them as necessary for your models. """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="linear low-density polyethylene", layercode="LLDPE", **extra ) def density(self, T=None): """ density of LLDPE: density(T in K) By default, uses an approximate value between LDPE and HDPE. """ T = self.T if T is None else check_units(T, None, "degC")[0] # Similar formula to LDPE and HDPE, with a coefficient suitable for LLDPE. return 915 * (1 - 3 * (T - layer._defaults["Td"]) * 15e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of LLDPE Typically close to LDPE, though slightly higher or lower can be found in the literature. """ return -120, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of LLDPE Typically close to LDPE, though slightly higher or lower can be found in the literature.
Expand source code
@property def Tg(self): """ glass transition temperature of LLDPE Typically close to LDPE, though slightly higher or lower can be found in the literature. """ return -120, "degC"
Methods
def density(self, T=None)
-
density of LLDPE: density(T in K) By default, uses an approximate value between LDPE and HDPE.
Expand source code
def density(self, T=None): """ density of LLDPE: density(T in K) By default, uses an approximate value between LDPE and HDPE. """ T = self.T if T is None else check_units(T, None, "degC")[0] # Similar formula to LDPE and HDPE, with a coefficient suitable for LLDPE. return 915 * (1 - 3 * (T - layer._defaults["Td"]) * 15e-5), "kg/m**3"
Inherited members
class PA6 (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PA6', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
PA6 layer constructor
Expand source code
class PA6(layer): _chemicalsubstance = "caprolactam" # monomer for polymers _polarityindex = 7.5 # Strong hydrogen-bonding, thus quite polar. """ extended pantankar.layer for polyamide 6 (PA6) """ def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in PA6",**extra): """ PA6 layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="polyamide 6", layercode="PA6", **extra ) def density(self, T=None): """ density of PA6: ~1140 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1140 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of PA6 """ return 50, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of PA6
Expand source code
@property def Tg(self): """ glass transition temperature of PA6 """ return 50, "degC"
Methods
def density(self, T=None)
-
density of PA6: ~1140 kg/m^3
Expand source code
def density(self, T=None): """ density of PA6: ~1140 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1140 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
Inherited members
class PA66 (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PA66', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
PA66 layer constructor
Expand source code
class PA66(layer): _chemicalsubstance = "hexamethylenediamine" # monomer for polymers _polarityindex = 7.5 # Similar to PA6, strongly polar with hydrogen bonds. """ extended pantankar.layer for polyamide 66 (PA66) """ def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in PA66",**extra): """ PA66 layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="polyamide 6,6", layercode="PA6,6", **extra ) def density(self, T=None): """ density of PA66: ~1150 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1150 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of PA66 """ return 70, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of PA66
Expand source code
@property def Tg(self): """ glass transition temperature of PA66 """ return 70, "degC"
Methods
def density(self, T=None)
-
density of PA66: ~1150 kg/m^3
Expand source code
def density(self, T=None): """ density of PA66: ~1150 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1150 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
Inherited members
class PBT (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PBT', **extra)
-
extended pantankar.layer for polybutylene terephthalate (PBT)
PBT layer constructor
Expand source code
class PBT(layer): """ extended pantankar.layer for polybutylene terephthalate (PBT) """ _chemicalsubstance = "Buthylene terephthalate" # monomer for polymers _polarityindex = 5.5 # Similar to PET, slight structural differences def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in PBT",**extra): """ PBT layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="polybutylene terephthalate", layercode="PBT", **extra ) def density(self, T=None): """ density of PBT: ~1310 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1310 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of PBT """ return 40, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of PBT
Expand source code
@property def Tg(self): """ glass transition temperature of PBT """ return 40, "degC"
Methods
def density(self, T=None)
-
density of PBT: ~1310 kg/m^3
Expand source code
def density(self, T=None): """ density of PBT: ~1310 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1310 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class PEN (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PEN', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
PEN layer constructor
Expand source code
class PEN(layer): _chemicalsubstance = "Buthylene terephthalate" # monomer for polymers _polarityindex = 6 # More aromatic than PET, often better barrier properties """ extended pantankar.layer for polyethylene naphthalate (PEN) """ def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in PEN",**extra): """ PEN layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="polyethylene naphthalate", layercode="PEN", **extra ) def density(self, T=None): """ density of PEN: ~1330 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1330 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of PEN """ return 120, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of PEN
Expand source code
@property def Tg(self): """ glass transition temperature of PEN """ return 120, "degC"
Methods
def density(self, T=None)
-
density of PEN: ~1330 kg/m^3
Expand source code
def density(self, T=None): """ density of PEN: ~1330 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1330 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class PP (l=0.0003, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PP', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
PP layer constructor
Expand source code
class PP(layer): _chemicalsubstance = "propylene" # monomer for polymers _polarityindex = 1.0 # Among the least polar, similar to PE """ extended pantankar.layer for isotactic polypropylene PP """ def __init__(self,l=300e-6,D=1e-14, T=None, k=None,C0=None,lunit=None,Dunit=None,kunit=None,Cunit=None, layername="layer in PP",**extra): """ PP layer constructor """ layer.__init__(self, l=l,D=D,k=k,C0=C0, T=T, lunit=lunit,Dunit=Dunit,kunit=kunit,Cunit=Cunit, layername=layername, layertype="polymer", # set by default at inititialization layermaterial="isotactic polypropylene", layercode="PP", **extra ) def density(self,T=None): """ density of PP: density(T in K) """ T = self.T if T is None else check_units(T,None,"degC")[0] return 910 *(1-3*(T-layer._defaults["Td"])*7e-5),"kg/m**3" # lowest temperature @property def Tg(self): """ glass transition temperature of PP """ return 0,"degC" # highest temperature
Ancestors
Instance variables
var Tg
-
glass transition temperature of PP
Expand source code
@property def Tg(self): """ glass transition temperature of PP """ return 0,"degC" # highest temperature
Methods
def density(self, T=None)
-
density of PP: density(T in K)
Expand source code
def density(self,T=None): """ density of PP: density(T in K) """ T = self.T if T is None else check_units(T,None,"degC")[0] return 910 *(1-3*(T-layer._defaults["Td"])*7e-5),"kg/m**3" # lowest temperature
Inherited members
class PPrubber (l=0.0001, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PPrubber', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
PPrubber layer constructor
Expand source code
class PPrubber(layer): _chemicalsubstance = "propylene" # monomer for polymers _polarityindex = 1.0 # Also very non-polar """ extended pantankar.layer for atactic (rubbery) polypropylene PP """ def __init__(self, l=100e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in PPrubber",**extra): """ PPrubber layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="atactic polypropylene", layercode="aPP", **extra ) def density(self, T=None): """ density of atactic (rubbery) PP: density(T in K) Approximate initial density ~900 kg/m^3, linear thermal expansion factor can be adjusted. """ T = self.T if T is None else check_units(T, None, "degC")[0] return 900 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of atactic/rubbery PP """ return -20, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of atactic/rubbery PP
Expand source code
@property def Tg(self): """ glass transition temperature of atactic/rubbery PP """ return -20, "degC"
Methods
def density(self, T=None)
-
density of atactic (rubbery) PP: density(T in K) Approximate initial density ~900 kg/m^3, linear thermal expansion factor can be adjusted.
Expand source code
def density(self, T=None): """ density of atactic (rubbery) PP: density(T in K) Approximate initial density ~900 kg/m^3, linear thermal expansion factor can be adjusted. """ T = self.T if T is None else check_units(T, None, "degC")[0] return 900 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class PS (l=0.0001, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PS', **extra)
-
extended pantankar.layer for polystyrene (PS)
PS layer constructor
Expand source code
class PS(layer): """ extended pantankar.layer for polystyrene (PS) """ _chemicalsubstance = "styrene" # monomer for polymers _polarityindex = 3.0 # Slightly more polar than polyolefins, but still considered relatively non-polar def __init__(self, l=100e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in PS",**extra): """ PS layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="polystyrene", layercode="PS", **extra ) def density(self, T=None): """ density of PS: ~1050 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1050 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of PS """ return 100, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of PS
Expand source code
@property def Tg(self): """ glass transition temperature of PS """ return 100, "degC"
Methods
def density(self, T=None)
-
density of PS: ~1050 kg/m^3
Expand source code
def density(self, T=None): """ density of PS: ~1050 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1050 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
Inherited members
class Paper (l=8e-05, D=1e-15, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='paper layer', **extra)
-
extended pantankar.layer for paper (cellulose-based)
Paper layer constructor
Expand source code
class Paper(layer): """ extended pantankar.layer for paper (cellulose-based) """ _physicalstate = "porous" # solid (default), liquid, gas, porous _chemicalclass = "other" # polymer (default), other _chemicalsubstance = "cellulose" # Cellulose (β-D-glucopyranose units) _polarityindex = 8.5 # Highly polar, strong hydrogen-bonding. def __init__(self, l=80e-6, D=1e-15, T=None, # a guess for barrier properties k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="paper layer",**extra): """ Paper layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="paper", layermaterial="paper", layercode="paper", **extra ) def density(self, T=None): """ approximate density for typical paper ~800 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 800 * (1 - 3*(T - layer._defaults["Td"]) * 1e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature is not typically used for paper, but we provide a placeholder. """ return 200, "degC" # purely illustrative placeholder
Ancestors
Instance variables
var Tg
-
glass transition temperature is not typically used for paper, but we provide a placeholder.
Expand source code
@property def Tg(self): """ glass transition temperature is not typically used for paper, but we provide a placeholder. """ return 200, "degC" # purely illustrative placeholder
Methods
def density(self, T=None)
-
approximate density for typical paper ~800 kg/m^3
Expand source code
def density(self, T=None): """ approximate density for typical paper ~800 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 800 * (1 - 3*(T - layer._defaults["Td"]) * 1e-5), "kg/m**3"
Inherited members
class SBS (l=0.0001, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in PBS', **extra)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
DBS layer constructor
Expand source code
class SBS(layer): _chemicalsubstance = "styrene" # Styrene + butadiene _polarityindex = 3.5 # Non-polar but somewhat more interactive than pure PE/PP due to styrene units """ extended pantankar.layer for a styrene-based SBS Adjust Tg/density as needed for your scenario. """ def __init__(self, l=100e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in PBS",**extra): """ DBS layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="styrene-based polymer SBS", layercode="SBS", **extra ) def density(self, T=None): """ density of 'DBS': approximate, around ~1030 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1030 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of 'DBS' """ return 90, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of 'DBS'
Expand source code
@property def Tg(self): """ glass transition temperature of 'DBS' """ return 90, "degC"
Methods
def density(self, T=None)
-
density of 'DBS': approximate, around ~1030 kg/m^3
Expand source code
def density(self, T=None): """ density of 'DBS': approximate, around ~1030 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1030 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
Inherited members
class SIbase (filename='', force_ndarray: bool = False, force_ndarray_like: bool = False, default_as_delta: bool = True, autoconvert_offset_to_baseunit: bool = False, on_redefinition: str = 'warn', system=None, auto_reduce_dimensions=False, preprocessors=None, fmt_locale=None, non_int_type=builtins.float, case_sensitive: bool = True)
-
The unit registry stores the definitions and relationships between units.
Parameters
- filename :
- path of the units definition file to load or line-iterable object.
- Empty to load the default definition file.
- None to leave the UnitRegistry empty.
force_ndarray
:bool
- convert any input, scalar or not to a numpy.ndarray.
force_ndarray_like
:bool
- convert all inputs other than duck arrays to a numpy.ndarray.
- default_as_delta :
- In the context of a multiplication of units, interpret
- non-multiplicative units as their delta counterparts.
- autoconvert_offset_to_baseunit :
- If True converts offset units in quantities are
- converted to their base units in multiplicative
- context. If False no conversion happens.
on_redefinition
:str
- action to take in case a unit is redefined. 'warn', 'raise', 'ignore'
- auto_reduce_dimensions :
- If True, reduce dimensionality on appropriate operations.
- preprocessors :
- list of callables which are iteratively ran on any input expression
- or unit string
- fmt_locale :
- locale identifier string, used in
format_babel
. Default to None case_sensitive
:bool
, optional- Control default case sensitivity of unit parsing. (Default: True)
Expand source code
class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): """The unit registry stores the definitions and relationships between units. Parameters ---------- filename : path of the units definition file to load or line-iterable object. Empty to load the default definition file. None to leave the UnitRegistry empty. force_ndarray : bool convert any input, scalar or not to a numpy.ndarray. force_ndarray_like : bool convert all inputs other than duck arrays to a numpy.ndarray. default_as_delta : In the context of a multiplication of units, interpret non-multiplicative units as their *delta* counterparts. autoconvert_offset_to_baseunit : If True converts offset units in quantities are converted to their base units in multiplicative context. If False no conversion happens. on_redefinition : str action to take in case a unit is redefined. 'warn', 'raise', 'ignore' auto_reduce_dimensions : If True, reduce dimensionality on appropriate operations. preprocessors : list of callables which are iteratively ran on any input expression or unit string fmt_locale : locale identifier string, used in `format_babel`. Default to None case_sensitive : bool, optional Control default case sensitivity of unit parsing. (Default: True) """ def __init__( self, filename="", force_ndarray: bool = False, force_ndarray_like: bool = False, default_as_delta: bool = True, autoconvert_offset_to_baseunit: bool = False, on_redefinition: str = "warn", system=None, auto_reduce_dimensions=False, preprocessors=None, fmt_locale=None, non_int_type=float, case_sensitive: bool = True, ): super().__init__( filename=filename, force_ndarray=force_ndarray, force_ndarray_like=force_ndarray_like, on_redefinition=on_redefinition, default_as_delta=default_as_delta, autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, system=system, auto_reduce_dimensions=auto_reduce_dimensions, preprocessors=preprocessors, fmt_locale=fmt_locale, non_int_type=non_int_type, case_sensitive=case_sensitive, ) def pi_theorem(self, quantities): """Builds dimensionless quantities using the Buckingham π theorem Parameters ---------- quantities : dict mapping between variable name and units Returns ------- list a list of dimensionless quantities expressed as dicts """ return pi_theorem(quantities, self) def setup_matplotlib(self, enable: bool = True) -> None: """Set up handlers for matplotlib's unit support. Parameters ---------- enable : bool whether support should be enabled or disabled (Default value = True) """ # Delays importing matplotlib until it's actually requested from .matplotlib import setup_matplotlib_handlers setup_matplotlib_handlers(self, enable) wraps = registry_helpers.wraps check = registry_helpers.check
Ancestors
- patankar.private.pint.registry.SystemRegistry
- patankar.private.pint.registry.ContextRegistry
- patankar.private.pint.registry.NonMultiplicativeRegistry
- patankar.private.pint.registry.BaseRegistry
Methods
def check(ureg: UnitRegistry, *args: Union[str, patankar.private.pint.util.UnitsContainer, ForwardRef('Unit'), NoneType]) ‑> Callable[[~F], ~F]
-
Decorator to for quantity type checking for function inputs.
Use it to ensure that the decorated function input parameters match the expected dimension of pint quantity.
The wrapper function raises: -
pint.DimensionalityError
if an argument doesn't match the required dimensions.ureg : UnitRegistry a UnitRegistry instance. args : str or UnitContainer or None Dimensions of each of the input arguments. Use
None
to skip argument conversion.Returns
callable
- the wrapped function.
Raises
TypeError
- If the number of given dimensions does not match the number of function parameters.
ValueError
- If the any of the provided dimensions cannot be parsed as a dimension.
Expand source code
def check( ureg: "UnitRegistry", *args: Union[str, UnitsContainer, "Unit", None] ) -> Callable[[F], F]: """Decorator to for quantity type checking for function inputs. Use it to ensure that the decorated function input parameters match the expected dimension of pint quantity. The wrapper function raises: - `pint.DimensionalityError` if an argument doesn't match the required dimensions. ureg : UnitRegistry a UnitRegistry instance. args : str or UnitContainer or None Dimensions of each of the input arguments. Use `None` to skip argument conversion. Returns ------- callable the wrapped function. Raises ------ TypeError If the number of given dimensions does not match the number of function parameters. ValueError If the any of the provided dimensions cannot be parsed as a dimension. """ dimensions = [ ureg.get_dimensionality(dim) if dim is not None else None for dim in args ] def decorator(func): count_params = len(signature(func).parameters) if len(dimensions) != count_params: raise TypeError( "%s takes %i parameters, but %i dimensions were passed" % (func.__name__, count_params, len(dimensions)) ) assigned = tuple( attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) ) updated = tuple( attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) ) @functools.wraps(func, assigned=assigned, updated=updated) def wrapper(*args, **kwargs): list_args, empty = _apply_defaults(func, args, kwargs) for dim, value in zip(dimensions, list_args): if dim is None: continue if not ureg.Quantity(value).check(dim): val_dim = ureg.get_dimensionality(value) raise DimensionalityError(value, "a quantity of", val_dim, dim) return func(*args, **kwargs) return wrapper return decorator
def pi_theorem(self, quantities)
-
Builds dimensionless quantities using the Buckingham π theorem
Parameters
quantities
:dict
- mapping between variable name and units
Returns
list
- a list of dimensionless quantities expressed as dicts
Expand source code
def pi_theorem(self, quantities): """Builds dimensionless quantities using the Buckingham π theorem Parameters ---------- quantities : dict mapping between variable name and units Returns ------- list a list of dimensionless quantities expressed as dicts """ return pi_theorem(quantities, self)
def setup_matplotlib(self, enable: bool = True) ‑> NoneType
-
Set up handlers for matplotlib's unit support.
Parameters
enable
:bool
- whether support should be enabled or disabled (Default value = True)
Expand source code
def setup_matplotlib(self, enable: bool = True) -> None: """Set up handlers for matplotlib's unit support. Parameters ---------- enable : bool whether support should be enabled or disabled (Default value = True) """ # Delays importing matplotlib until it's actually requested from .matplotlib import setup_matplotlib_handlers setup_matplotlib_handlers(self, enable)
def wraps(ureg: UnitRegistry, ret: Union[str, ForwardRef('Unit'), Iterable[Union[str, ForwardRef('Unit'), NoneType]], NoneType], args: Union[str, ForwardRef('Unit'), Iterable[Union[str, ForwardRef('Unit'), NoneType]], NoneType], strict: bool = True) ‑> Callable[[Callable[..., ~T]], Callable[..., patankar.private.pint.quantity.Quantity[~T]]]
-
Wraps a function to become pint-aware.
Use it when a function requires a numerical value but in some specific units. The wrapper function will take a pint quantity, convert to the units specified in
args
and then call the wrapped function with the resulting magnitude.The value returned by the wrapped function will be converted to the units specified in
ret
.Parameters
ureg
:pint.UnitRegistry
- a UnitRegistry instance.
ret
:str, pint.Unit,
oriterable
ofstr
orpint.Unit
- Units of each of the return values. Use
None
to skip argument conversion. args
:str, pint.Unit,
oriterable
ofstr
orpint.Unit
- Units of each of the input arguments. Use
None
to skip argument conversion. strict
:bool
- Indicates that only quantities are accepted. (Default value = True)
Returns
callable
- the wrapper function.
Raises
TypeError
- if the number of given arguments does not match the number of function parameters. if any of the provided arguments is not a unit a string or Quantity
Expand source code
def wraps( ureg: "UnitRegistry", ret: Union[str, "Unit", Iterable[Union[str, "Unit", None]], None], args: Union[str, "Unit", Iterable[Union[str, "Unit", None]], None], strict: bool = True, ) -> Callable[[Callable[..., T]], Callable[..., Quantity[T]]]: """Wraps a function to become pint-aware. Use it when a function requires a numerical value but in some specific units. The wrapper function will take a pint quantity, convert to the units specified in `args` and then call the wrapped function with the resulting magnitude. The value returned by the wrapped function will be converted to the units specified in `ret`. Parameters ---------- ureg : pint.UnitRegistry a UnitRegistry instance. ret : str, pint.Unit, or iterable of str or pint.Unit Units of each of the return values. Use `None` to skip argument conversion. args : str, pint.Unit, or iterable of str or pint.Unit Units of each of the input arguments. Use `None` to skip argument conversion. strict : bool Indicates that only quantities are accepted. (Default value = True) Returns ------- callable the wrapper function. Raises ------ TypeError if the number of given arguments does not match the number of function parameters. if any of the provided arguments is not a unit a string or Quantity """ if not isinstance(args, (list, tuple)): args = (args,) for arg in args: if arg is not None and not isinstance(arg, (ureg.Unit, str)): raise TypeError( "wraps arguments must by of type str or Unit, not %s (%s)" % (type(arg), arg) ) converter = _parse_wrap_args(args) is_ret_container = isinstance(ret, (list, tuple)) if is_ret_container: for arg in ret: if arg is not None and not isinstance(arg, (ureg.Unit, str)): raise TypeError( "wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(arg), arg) ) ret = ret.__class__([_to_units_container(arg, ureg) for arg in ret]) else: if ret is not None and not isinstance(ret, (ureg.Unit, str)): raise TypeError( "wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(ret), ret) ) ret = _to_units_container(ret, ureg) def decorator(func: Callable[..., T]) -> Callable[..., Quantity[T]]: count_params = len(signature(func).parameters) if len(args) != count_params: raise TypeError( "%s takes %i parameters, but %i units were passed" % (func.__name__, count_params, len(args)) ) assigned = tuple( attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) ) updated = tuple( attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) ) @functools.wraps(func, assigned=assigned, updated=updated) def wrapper(*values, **kw) -> Quantity[T]: values, kw = _apply_defaults(func, values, kw) # In principle, the values are used as is # When then extract the magnitudes when needed. new_values, values_by_name = converter(ureg, values, strict) result = func(*new_values, **kw) if is_ret_container: out_units = ( _replace_units(r, values_by_name) if is_ref else r for (r, is_ref) in ret ) return ret.__class__( res if unit is None else ureg.Quantity(res, unit) for unit, res in zip_longest(out_units, result) ) if ret[0] is None: return result return ureg.Quantity( result, _replace_units(ret[0], values_by_name) if ret[1] else ret[0] ) return wrapper return decorator
class air (l=0.01, D=1e-06, T=None, lunit=None, Dunit=None, Cunit=None, layername='air layer', layercode='air', **extra)
-
extended pantankar.layer for ideal gases such as air
air layer constructor
Expand source code
class air(layer): """ extended pantankar.layer for ideal gases such as air """ _physicalstate = "gas" # solid (default), liquid, gas, porous _chemicalclass = "other" # polymer (default), other def __init__(self,l=1e-2,D=1e-6,T=None, lunit=None,Dunit=None,Cunit=None, layername="air layer",layercode="air",**extra): """ air layer constructor """ T = layer._defaults["T"] if T is None else check_units(T,None,"degC")[0] TK = constants["T0K"]+T kair = 1/(constants["R"] *TK) kairunit = constants["iRT0Kunit"] layer.__init__(self, l=l,D=D,k=kair,C0=0,T=T, lunit=lunit,Dunit=Dunit,kunit=kairunit,Cunit=Cunit, layername=layername, layertype="air", # set by default at inititialization layermaterial="ideal gas", layercode="gas", **extra ) def density(self, T=None): """Density of air at atmospheric pressure: density(T in K)""" TK = self.TK if T is None else check_units(T,None,"K")[0] P_atm = 101325 # Pa (1 atm) M_air = 28.9647 # g/mol = 0.0289647 kg/mol (Molar mass of dry air). return P_atm / ((constants["R"]/M_air) * TK), "kg/m**3"
Ancestors
Methods
def density(self, T=None)
-
Density of air at atmospheric pressure: density(T in K)
Expand source code
def density(self, T=None): """Density of air at atmospheric pressure: density(T in K)""" TK = self.TK if T is None else check_units(T,None,"K")[0] P_atm = 101325 # Pa (1 atm) M_air = 28.9647 # g/mol = 0.0289647 kg/mol (Molar mass of dry air). return P_atm / ((constants["R"]/M_air) * TK), "kg/m**3"
Inherited members
class qSI (value, units=None)
-
Implements a class to describe a physical quantity: the product of a numerical value and a unit of measurement.
Parameters
value
:str, pint.Quantity
orany numeric type
- Value of the physical quantity to be created.
units
:UnitsContainer, str
orpint.Quantity
- Units of the physical quantity to be created.
Returns
Expand source code
class Quantity(_Quantity): _REGISTRY = registry
Ancestors
- patankar.private.pint.quantity.Quantity
- patankar.private.pint.util.PrettyIPython
- patankar.private.pint.util.SharedRegistryObject
- typing.Generic
class gPET (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in gPET', **extra)
-
extended pantankar.layer for PET in its glassy state (below ~76°C)
glassy PET layer constructor
Expand source code
class gPET(layer): """ extended pantankar.layer for PET in its glassy state (below ~76°C) """ _chemicalsubstance = "ethylene terephthalate" # monomer for polymers _polarityindex = 5.0 # Polyester with significant dipolar interactions (Ph = phenylene ring). def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in gPET",**extra): """ glassy PET layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="glassy PET", layercode="PET", **extra ) def density(self, T=None): """ density of glassy PET: ~1350 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1350 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ approximate glass transition temperature of PET """ return 76, "degC"
Ancestors
Instance variables
var Tg
-
approximate glass transition temperature of PET
Expand source code
@property def Tg(self): """ approximate glass transition temperature of PET """ return 76, "degC"
Methods
def density(self, T=None)
-
density of glassy PET: ~1350 kg/m^3
Expand source code
def density(self, T=None): """ density of glassy PET: ~1350 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1350 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class layer (l=None, D=None, k=None, C0=None, rho=None, T=None, lunit=None, Dunit=None, kunit=None, Cunit=None, rhounit=None, Tunit=None, layername=None, layertype=None, layermaterial=None, layercode=None, substance=None, medium=None, nmesh=None, nmeshmin=None, Dlink=None, klink=None, C0link=None, Tlink=None, llink=None, verbose=None, verbosity=2, **unresolved)
-
Core Functionality
This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the
+
operator and support dynamic property linkage usinglayerLink
.
Key Properties
l
: Thickness of the layer (m)D
: Diffusion coefficient (m²/s)k
: Partition coefficient (dimensionless)C0
: Initial concentration (arbitrary units)rho
: Density (kg/m³)T
: Contact temperature (°C)substance
: Migrant/substance modeled for diffusionmedium
: The food medium in contact with the layerDmodel
,kmodel
: Callable models for diffusion and partitioning
Methods
__add__(self, other)
: Combines two layers into a multilayer structure.__mul__(self, n)
: Duplicates a layern
times to create a multilayer.__getitem__(self, i)
: Retrieves a sublayer from a multilayer.__setitem__(self, i, other)
: Replaces sublayers in a multilayer structure.mesh(self)
: Generates a numerical mesh for finite-volume simulations.struct(self)
: Returns a dictionary representation of the layer properties.resolvename(param_value, param_key, **unresolved)
: Resolves synonyms for parameter names.help(cls)
: Displays a dynamically formatted summary of input parameters.
Integration with SFPPy Modules
- Works with
migration.py
for mass transfer simulations. - Interfaces with
food.py
to define food-contact conditions. - Uses
property.py
for predicting diffusion (D
) and partitioning (k
). - Connects with
geometry.py
for 3D packaging simulations.
Usage Example
from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF()
Notes
- This class supports dynamic property inheritance, meaning
D
andk
can be computed based on the substance defined insubstance
andmedium
. - The
layerLink
mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures accurate steady-state and transient behavior.
Parameters
layername
:TYPE
, optional, string
- DESCRIPTION. Layer Name. The default is "my layer".
layertype
:TYPE
, optional, string
- DESCRIPTION. Layer Type. The default is "unknown type".
layermaterial
:TYPE
, optional, string
- DESCRIPTION. Material identification . The default is "unknown material".
- PHYSICAL QUANTITIES
l
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Thickness. The default is 50e-6 (m).
D
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s).
k
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.).
C0
:TYPE
, optional, scalar
ortupple (value,"unit")
- DESCRIPTION. Initial concentration. The default is 1000 (a.u.).
- PHYSICAL UNITS
lunit
:TYPE
, optional, string
- DESCRIPTION. Length units. The default unit is "m.
Dunit
:TYPE
, optional, string
- DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s".
kunit
:TYPE
, optional, string
- DESCRIPTION. Henry-like coefficient. The default unit is "a.u.".
Cunit
:TYPE
, optional, string
- DESCRIPTION. Initial concentration. The default unit is "a.u.".
Returns
a monolayer object which can be assembled into a multilayer structure
Expand source code
class layer: """ ------------------------------------------------------------------------------ **Core Functionality** ------------------------------------------------------------------------------ This class models layers in food packaging, handling mass transfer, partitioning, and meshing for finite-volume simulations using a modified Patankar method. Layers can be assembled into multilayers via the `+` operator and support dynamic property linkage using `layerLink`. ------------------------------------------------------------------------------ **Key Properties** ------------------------------------------------------------------------------ - `l`: Thickness of the layer (m) - `D`: Diffusion coefficient (m²/s) - `k`: Partition coefficient (dimensionless) - `C0`: Initial concentration (arbitrary units) - `rho`: Density (kg/m³) - `T`: Contact temperature (°C) - `substance`: Migrant/substance modeled for diffusion - `medium`: The food medium in contact with the layer - `Dmodel`, `kmodel`: Callable models for diffusion and partitioning ------------------------------------------------------------------------------ **Methods** ------------------------------------------------------------------------------ - `__add__(self, other)`: Combines two layers into a multilayer structure. - `__mul__(self, n)`: Duplicates a layer `n` times to create a multilayer. - `__getitem__(self, i)`: Retrieves a sublayer from a multilayer. - `__setitem__(self, i, other)`: Replaces sublayers in a multilayer structure. - `mesh(self)`: Generates a numerical mesh for finite-volume simulations. - `struct(self)`: Returns a dictionary representation of the layer properties. - `resolvename(param_value, param_key, **unresolved)`: Resolves synonyms for parameter names. - `help(cls)`: Displays a dynamically formatted summary of input parameters. ------------------------------------------------------------------------------ **Integration with SFPPy Modules** ------------------------------------------------------------------------------ - Works with `migration.py` for mass transfer simulations. - Interfaces with `food.py` to define food-contact conditions. - Uses `property.py` for predicting diffusion (`D`) and partitioning (`k`). - Connects with `geometry.py` for 3D packaging simulations. ------------------------------------------------------------------------------ **Usage Example** ------------------------------------------------------------------------------ ```python from patankar.layer import LDPE, PP, layerLink # Define a polymer layer with default properties A = LDPE(l=50e-6, D=1e-14) # Create a multilayer structure B = PP(l=200e-6, D=1e-15) multilayer = A + B # Assign dynamic property linkage k_link = layerLink("k", indices=[1], values=[10]) # Assign partition coefficient to the second layer multilayer.klink = k_link # Simulate migration from patankar.migration import senspatankar from patankar.food import ethanol medium = ethanol() solution = senspatankar(multilayer, medium) solution.plotCF() ``` ------------------------------------------------------------------------------ **Notes** ------------------------------------------------------------------------------ - This class supports dynamic property inheritance, meaning `D` and `k` can be computed based on the substance defined in `substance` and `medium`. - The `layerLink` mechanism allows parameter adjustments without modifying the core object. - The modified finite-volume meshing ensures **accurate steady-state and transient** behavior. """ # ----------------------------------------------------------------------------- # Class attributes that can be overidden in instances. # Their default values are set in classes and overriden with similar # instance properties with @property.setter. # These values cannot be set during construction, but only after instantiation. # ----------------------------------------------------------------------------- # These properties are essential for model predictions, they cannot be customized # beyond the rules accepted by the model predictors (they are not metadata) _physicalstate = "solid" # solid (default), liquid, gas, porous _chemicalclass = "polymer" # polymer (default), other _chemicalsubstance = None # None (default), monomer for polymers _polarityindex = 0.0 # polarity index (roughly: 0=hexane, 10=water) # Low-level prediction properties (these properties are common with patankar.food) _lowLevelPredictionPropertyList = ["physicalstate","chemicalclass", "chemicalsubstance","polarityindex","ispolymer","issolid"] # -------------------------------------------------------------------- # PRIVATE PROPERTIES (cannot be changed by the user) # __ read only attributes # _ private attributes (not public) # -------------------------------------------------------------------- __description = "LAYER object" # description __version = 1.0 # version __contact = "olivier.vitrac@agroparistech.fr" # contact person _printformat = "%0.4g" # format to display D, k, l values # Synonyms dictionary: Maps alternative names to the actual parameter # these synonyms can be used during construction _synonyms = { "substance": {"migrant", "compound", "chemical","molecule","solute"}, "medium": {"food","simulant","fluid","liquid","contactmedium"}, "C0": {"CP0", "Cp0"}, "l": {"lp", "lP"}, "D": {"Dp", "DP"}, "k": {"kp", "kP"}, "T": {"temp","Temp","temperature","Temperature", "contacttemperature","ContactTemperature","contactTemperature"} } # Default values for parameters (note that Td cannot be changed by the end-user) _defaults = { "l": 5e-5, # Thickness (m) "D": 1e-14, # Diffusion coefficient (m^2/s) "k": 1.0, # Henri-like coefficient (dimensionless) "C0": 1000, # Initial concentration (arbitrary units) "rho": 1000, # Default density (kg/m³) "T": 40.0, # Default temperature (°C) "Td": 25.0, # Reference temperature for densities (°C) # Units (do not change) "lunit": "m", "Dunit": "m**2/s", "kunit": "a.u.", # NoUnits "Cunit": "a.u.", # NoUnits "rhounit": "kg/m**3", "Tunit": "degC", # Temperatures are indicated in °C instead of K (to reduce end-user mistakes) # Layer properties "layername": "my layer", "layertype": "unknown type", "layermaterial": "unknown material", "layercode": "N/A", # Mesh parameters "nmeshmin": 20, "nmesh": 600, # Substance "substance": None, "simulant": None, # Other parameters "verbose": None, "verbosity": 2 } # List units _parametersWithUnits = { "l": "m", "D": "m**2/s", "k": "a.u.", "C": "a.u.", "rhp": "kg/m**3", "T": "degC", } # Brief descriptions for each parameter _descriptionInputs = { "l": "Thickness of the layer (m)", "D": "Diffusion coefficient (m²/s)", "k": "Henri-like coefficient (dimensionless)", "C0": "Initial concentration (arbitrary units)", "rho": "Density of the material (kg/m³)", "T": "Layer temperature (°C)", "Td": "Reference temperature for densities (°C)", "lunit": "Unit of thickness (default: m)", "Dunit": "Unit of diffusion coefficient (default: m²/s)", "kunit": "Unit of Henri-like coefficient (default: a.u.)", "Cunit": "Unit of initial concentration (default: a.u.)", "rhounit": "Unit of density (default: kg/m³)", "Tunit": "Unit of temperature (default: degC)", "layername": "Name of the layer", "layertype": "Type of layer (e.g., polymer, ink, air)", "layermaterial": "Material composition of the layer", "layercode": "Identification code for the layer", "nmeshmin": "Minimum number of FV mesh elements for the layer", "nmesh": "Number of FV mesh elements for numerical computation", "verbose": "Verbose mode (None or boolean)", "verbosity": "Level of verbosity for debug messages (integer)" } # -------------------------------------------------------------------- # CONSTRUCTOR OF INSTANCE PROPERTIES # None = missing numeric value (managed by default) # -------------------------------------------------------------------- def __init__(self, l=None, D=None, k=None, C0=None, rho=None, T=None, lunit=None, Dunit=None, kunit=None, Cunit=None, rhounit=None, Tunit=None, layername=None,layertype=None,layermaterial=None,layercode=None, substance = None, medium = None, # Dmodel = None, kmodel = None, they are defined via migrant (future overrides) nmesh=None, nmeshmin=None, # simulation parametes # link properties (for fitting and linking properties across simulations) Dlink=None, klink=None, C0link=None, Tlink=None, llink=None, verbose=None, verbosity=2,**unresolved): """ Parameters ---------- layername : TYPE, optional, string DESCRIPTION. Layer Name. The default is "my layer". layertype : TYPE, optional, string DESCRIPTION. Layer Type. The default is "unknown type". layermaterial : TYPE, optional, string DESCRIPTION. Material identification . The default is "unknown material". PHYSICAL QUANTITIES l : TYPE, optional, scalar or tupple (value,"unit") DESCRIPTION. Thickness. The default is 50e-6 (m). D : TYPE, optional, scalar or tupple (value,"unit") DESCRIPTION. Diffusivity. The default is 1e-14 (m^2/s). k : TYPE, optional, scalar or tupple (value,"unit") DESCRIPTION. Henry-like coefficient. The default is 1 (a.u.). C0 : TYPE, optional, scalar or tupple (value,"unit") DESCRIPTION. Initial concentration. The default is 1000 (a.u.). PHYSICAL UNITS lunit : TYPE, optional, string DESCRIPTION. Length units. The default unit is "m. Dunit : TYPE, optional, string DESCRIPTION. Diffusivity units. The default unit is 1e-14 "m^2/s". kunit : TYPE, optional, string DESCRIPTION. Henry-like coefficient. The default unit is "a.u.". Cunit : TYPE, optional, string DESCRIPTION. Initial concentration. The default unit is "a.u.". Returns ------- a monolayer object which can be assembled into a multilayer structure """ # resolve alternative names used by end-users substance = layer.resolvename(substance,"substance",**unresolved) medium = layer.resolvename(medium, "medium", **unresolved) C0 = layer.resolvename(C0,"C0",**unresolved) l = layer.resolvename(l,"l",**unresolved) D = layer.resolvename(D,"D",**unresolved) k = layer.resolvename(k,"k",**unresolved) T = layer.resolvename(T,"T",**unresolved) # Assign defaults only if values are not provided l = l if l is not None else layer._defaults["l"] D = D if D is not None else layer._defaults["D"] k = k if k is not None else layer._defaults["k"] C0 = C0 if C0 is not None else layer._defaults["C0"] rho = rho if rho is not None else layer._defaults["rho"] T = T if T is not None else layer._defaults["T"] lunit = lunit if lunit is not None else layer._defaults["lunit"] Dunit = Dunit if Dunit is not None else layer._defaults["Dunit"] kunit = kunit if kunit is not None else layer._defaults["kunit"] Cunit = Cunit if Cunit is not None else layer._defaults["Cunit"] rhounit = rhounit if rhounit is not None else layer._defaults["rhounit"] Tunit = Tunit if Tunit is not None else layer._defaults["Tunit"] nmesh = nmesh if nmesh is not None else layer._defaults["nmesh"] nmeshmin = nmeshmin if nmeshmin is not None else layer._defaults["nmeshmin"] verbose = verbose if verbose is not None else layer._defaults["verbose"] verbosity = verbosity if verbosity is not None else layer._defaults["verbosity"] # Assign layer id properties layername = layername if layername is not None else layer._defaults["layername"] layertype = layertype if layertype is not None else layer._defaults["layertype"] layermaterial = layermaterial if layermaterial is not None else layer._defaults["layermaterial"] layercode = layercode if layercode is not None else layer._defaults["layercode"] # validate all physical paramaters with their units l,lunit = check_units(l,lunit,layer._defaults["lunit"]) D,Dunit = check_units(D,Dunit,layer._defaults["Dunit"]) k,kunit = check_units(k,kunit,layer._defaults["kunit"]) C0,Cunit = check_units(C0,Cunit,layer._defaults["Cunit"]) rho,rhounit = check_units(rho,rhounit,layer._defaults["rhounit"]) T,Tunit = check_units(T,Tunit,layer._defaults["Tunit"]) # set attributes: id and physical properties self._name = [layername] self._type = [layertype] self._material = [layermaterial] self._code = [layercode] self._nlayer = 1 self._l = l[:1] self._D = D[:1] self._k = k[:1] self._C0 = C0[:1] self._rho = rho[:1] self._T = T self._lunit = lunit self._Dunit = Dunit self._kunit = kunit self._Cunit = Cunit self._rhounit = rhounit self._Tunit = Tunit self._nmesh = nmesh self._nmeshmin = nmeshmin # intialize links for X = D,k,C0,T,l (see documentation of layerLink) # A link enables the values of X to be defined and controlled outside the instance self._Dlink = self._initialize_link(Dlink, "D") self._klink = self._initialize_link(klink, "k") self._C0link = self._initialize_link(C0link, "C0") self._Tlink = self._initialize_link(Tlink, "T") self._llink = self._initialize_link(llink, "l") # set substance, medium and related D and k models if isinstance(substance,str): substance = migrant(substance) if substance is not None and not isinstance(substance,migrant): raise ValueError(f"subtance must be None a or a migrant not a {type(substance).__name__}") self._substance = substance if medium is not None: from patankar.food import foodlayer # local import only if needed if not isinstance(medium,foodlayer): raise ValueError(f"medium must be None or a foodlayer not a {type(medium).__name__}") self._medium = medium self._Dmodel = "default" # do not use directly self._compute_Dmodel (force refresh) self._kmodel = "default" # do not use directly self._compute_kmodel (force refresh) # set history for all layers merged with + self._layerclass_history = [] self._ispolymer_history = [] self._chemicalsubstance_history = [] # set verbosity attributes self.verbosity = 0 if verbosity is None else verbosity self.verbose = verbosity>0 if verbose is None else verbose # we initialize the acknowlegment process for future property propagation self._hasbeeninherited = {} # -------------------------------------------------------------------- # Helper method: initializes and validates layerLink attributes # (Dlink, klink, C0link, Tlink, llink) # -------------------------------------------------------------------- def _initialize_link(self, link, expected_property): """ Initializes and validates a layerLink attribute. Parameters: ---------- link : layerLink or None The `layerLink` instance to be assigned. expected_property : str The expected property name (e.g., "D", "k", "C0", "T"). Returns: ------- layerLink or None The validated `layerLink` instance or None. Raises: ------- TypeError: If `link` is not a `layerLink` or `None`. ValueError: If `link.property` does not match `expected_property`. """ if link is None: return None if isinstance(link, layerLink): if link.property == expected_property: return link raise ValueError(f'{expected_property}link.property should be "{expected_property}" not "{link.property}"') raise TypeError(f"{expected_property}link must be a layerLink not a {type(link).__name__}") # -------------------------------------------------------------------- # Class method returning help() for the end user # -------------------------------------------------------------------- @classmethod def help(cls): """ Prints a dynamically formatted summary of all input parameters, adjusting column widths based on content and wrapping long descriptions. """ # Column Headers headers = ["Parameter", "Default Value", "Has Synonyms?", "Description"] col_widths = [len(h) for h in headers] # Start with header widths # Collect Data Rows rows = [] for param, default in cls._defaults.items(): has_synonyms = "✅ Yes" if param in cls._synonyms else "❌ No" description = cls._descriptionInputs.get(param, "No description available") # Update column widths dynamically col_widths[0] = max(col_widths[0], len(param)) col_widths[1] = max(col_widths[1], len(str(default))) col_widths[2] = max(col_widths[2], len(has_synonyms)) col_widths[3] = max(col_widths[3], len(description)) rows.append([param, str(default), has_synonyms, description]) # Function to wrap text for a given column width def wrap_text(text, width): return textwrap.fill(text, width) # Print Table with Adjusted Column Widths separator = "+-" + "-+-".join("-" * w for w in col_widths) + "-+" print("\n### **Accepted Parameters and Defaults**\n") print(separator) print("| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + " |") print(separator) for row in rows: # Wrap text in the description column row[3] = wrap_text(row[3], col_widths[3]) # Print row print("| " + " | ".join(row[i].ljust(col_widths[i]) for i in range(3)) + " | " + row[3]) print(separator) # Synonyms Table print("\n### **Parameter Synonyms**\n") syn_headers = ["Parameter", "Synonyms"] syn_col_widths = [ max(len("Parameter"), max(len(k) for k in cls._synonyms.keys())), # Ensure it fits "Parameter" max(len("Synonyms"), max(len(", ".join(v)) for v in cls._synonyms.values())) # Ensure it fits "Synonyms" ] syn_separator = "+-" + "-+-".join("-" * w for w in syn_col_widths) + "-+" print(syn_separator) print("| " + " | ".join(h.ljust(syn_col_widths[i]) for i, h in enumerate(syn_headers)) + " |") print(syn_separator) for param, synonyms in cls._synonyms.items(): print(f"| {param.ljust(syn_col_widths[0])} | {', '.join(synonyms).ljust(syn_col_widths[1])} |") print(syn_separator) # -------------------------------------------------------------------- # Class method to handle ambiguous definitions from end-user # -------------------------------------------------------------------- @classmethod def resolvename(cls, param_value, param_key, **unresolved): """ Resolves the correct parameter value using known synonyms. - If param_value is already set (not None), return it. - If a synonym exists in **unresolved, assign its value. - If multiple synonyms of the same parameter appear in **unresolved, raise an error. - Otherwise, return None. Parameters: - `param_name` (any): The original value (if provided). - `param_key` (str): The legitimate parameter name we are resolving. - `unresolved` (dict): The dictionary of unrecognized keyword arguments. Returns: - The resolved value or None if not found. """ if param_value is not None: return param_value # The parameter is explicitly defined, do not override if not unresolved: # shortcut return None resolved_value = None found_keys = [] # Check if param_key itself is present in unresolved if param_key in unresolved: found_keys.append(param_key) resolved_value = unresolved[param_key] # Check if any of its synonyms are in unresolved if param_key in cls._synonyms: for synonym in cls._synonyms[param_key]: if synonym in unresolved: found_keys.append(synonym) resolved_value = unresolved[synonym] # Raise error if multiple synonyms were found if len(found_keys) > 1: raise ValueError( f"Conflicting definitions: Multiple synonyms {found_keys} were provided for '{param_key}'." ) return resolved_value # -------------------------------------------------------------------- # overloading binary addition (note that the output is of type layer) # -------------------------------------------------------------------- def __add__(self, other): """ C = A + B | overload + operator """ if isinstance(other, layer): res = duplicate(self) res._nmeshmin = min(self._nmeshmin, other._nmeshmin) # Propagate substance if self._substance is None: res._substance = other._substance else: if isinstance(self._substance, migrant) and isinstance(other._substance, migrant): if self._substance.M != other._substance.M: print("Warning: the smallest substance is propagated everywhere") res._substance = self._substance if self._substance.M <= other._substance.M else other._substance else: res._substance = None # Concatenate general attributes for p in ["_name", "_type", "_material", "_code", "_nlayer"]: setattr(res, p, getattr(self, p) + getattr(other, p)) # Concatenate numeric arrays for p in ["_l", "_D", "_k", "_C0", "_rho", "_T"]: setattr(res, p, np.concatenate((getattr(self, p), getattr(other, p)))) # Handle history tracking res._layerclass_history = self.layerclass_history + other.layerclass_history res._ispolymer_history = self.ispolymer_history + other.ispolymer_history res._chemicalsubstance_history = self.chemicalsubstance_history + other.chemicalsubstance_history # Manage layerLink attributes (Dlink, klink, C0link, Tlink, llink) property_map = { "Dlink": ("D", self.Dlink, other.Dlink), "klink": ("k", self.klink, other.klink), "C0link": ("C0", self.C0link, other.C0link), "Tlink": ("T", self.Tlink, other.Tlink), "llink": ("l", self.llink, other.llink), } for attr, (prop, self_link, other_link) in property_map.items(): if (self_link is not None) and (other_link is not None): # Case 1: Both have a link → Apply `+` setattr(res, '_'+attr, self_link + other_link) elif self_link is not None: # Case 2: Only self has a link → Use as-is setattr(res, '_'+attr, self_link) elif other_link is not None: # Case 3: Only other has a link → Shift indices and use shifted_link = duplicate(other_link) shifted_link.indices += len(getattr(self, prop)) setattr(res, '_'+attr, shifted_link) else: # Case 4: Neither has a link → Result is None setattr(res, '_'+attr, None) return res else: raise ValueError("Invalid layer object") # -------------------------------------------------------------------- # overloading binary multiplication (note that the output is of type layer) # -------------------------------------------------------------------- def __mul__(self,ntimes): """ nA = A*n | overload * operator """ if isinstance(ntimes, int) and ntimes>0: res = duplicate(self) if ntimes>1: for n in range(1,ntimes): res += self return res else: raise ValueError("multiplicator should be a strictly positive integer") # -------------------------------------------------------------------- # len method # -------------------------------------------------------------------- def __len__(self): """ length method """ return self._nlayer # -------------------------------------------------------------------- # object indexing (get,set) method # -------------------------------------------------------------------- def __getitem__(self,i): """ get indexing method """ res = duplicate(self) # check indices isscalar = isinstance(i,int) if isinstance(i,slice): if i.step==None: j = list(range(i.start,i.stop)) else: j = list(range(i.start,i.stop,i.step)) res._nlayer = len(j) if isinstance(i,int): res._nlayer = 1 # pick indices for each property for p in ["_name","_type","_material","_l","_D","_k","_C0"]: content = getattr(self,p) try: if isscalar: setattr(res,p,content[i:i+1]) else: setattr(res,p,content[i]) except IndexError as err: if self.verbosity>0 and self.verbose: print("bad layer object indexing: ",err) return res def __setitem__(self,i,other): """ set indexing method """ # check indices if isinstance(i,slice): if i.step==None: j = list(range(i.start,i.stop)) else: j = list(range(i.start,i.stop,i.step)) elif isinstance(i,int): j = [i] else:raise IndexError("invalid index") islayer = isinstance(other,layer) isempty = not islayer and isinstance(other,list) and len(other)<1 if isempty: # empty right hand side for p in ["_name","_type","_material","_l","_D","_k","_C0"]: content = getattr(self,p) try: newcontent = [content[k] for k in range(self._nlayer) if k not in j] except IndexError as err: if self.verbosity>0 and self.verbose: print("bad layer object indexing: ",err) if isinstance(content,np.ndarray) and not isinstance(newcontent,np.ndarray): newcontent = np.array(newcontent) setattr(self,p,newcontent) self._nlayer = len(newcontent) elif islayer: # islayer right hand side nk1 = len(j) nk2 = other._nlayer if nk1 != nk2: raise IndexError("the number of elements does not match the number of indices") for p in ["_name","_type","_material","_l","_D","_k","_C0"]: content1 = getattr(self,p) content2 = getattr(other,p) for k in range(nk1): try: content1[j[k]] = content2[k] except IndexError as err: if self.verbosity>0 and self.verbose: print("bad layer object indexing: ",err) setattr(self,p,content1) else: raise ValueError("only [] or layer object are accepted") # -------------------------------------------------------------------- # Getter methods (show private/hidden properties and meta-properties) # -------------------------------------------------------------------- # Return class or instance attributes @property def physicalstate(self): return self._physicalstate @property def chemicalclass(self): return self._chemicalclass @property def chemicalsubstance(self): return self._chemicalsubstance @property def polarityindex(self): # rescaled to match predictions - standard scale [0,10.2] - predicted scale [0,7.12] return self._polarityindex * migrant("water").polarityindex/10.2 @property def ispolymer(self): return self.chemicalclass == "polymer" @property def issolid(self): return self.physicalstate == "solid" @property def layerclass_history(self): return self._layerclass_history if self._layerclass_history != [] else [self.layerclass] @property def ispolymer_history(self): return self._ispolymer_history if self._ispolymer_history != [] else [self.ispolymer] @property def chemicalsubstance_history(self): return self._chemicalsubstance_history if self._chemicalsubstance_history != [] else [self.chemicalsubstance] @property def layerclass(self): return type(self).__name__ @property def name(self): return self._name @property def type(self): return self._type @property def material(self): return self._material @property def code(self): return self._code @property def l(self): return self._l if not self.hasllink else self.llink.getfull(self._l) @property def D(self): Dtmp = None if self.Dmodel == "default": # default behavior Dtmp = self._compute_Dmodel() elif callable(self.Dmodel): # user override Dtmp = self.Dmodel() if Dtmp is not None: Dtmp = np.full_like(self._D, Dtmp,dtype=np.float64) if self.hasDlink: return self.Dlink.getfull(Dtmp) # substitution rules are applied as defined in Dlink else: return Dtmp return self._D if not self.hasDlink else self.Dlink.getfull(self._D) @property def k(self): ktmp = None if self.kmodel == "default": # default behavior ktmp = self._compute_kmodel() elif callable(self.kmodel): # user override ktmp = self.kmodel() if ktmp is not None: ktmp = np.full_like(self._k, ktmp,dtype=np.float64) if self.hasklink: return self.klink.getfull(ktmp) # substitution rules are applied as defined in klink else: return ktmp return self._k if not self.hasklink else self.klink.getfull(self._k) @property def C0(self): return self._C0 if not self.hasC0link else self.COlink.getfull(self._C0) @property def rho(self): return self._rho @property def T(self): return self._T if not self.hasTlink else self.Tlink.getfull(self._T) @property def TK(self): return self._T+T0K @property def lunit(self): return self._lunit @property def Dunit(self): return self._Dunit @property def kunit(self): return self._kunit @property def Cunit(self): return self._Cunit @property def rhounit(self): return self._rhounit @property def Tunit(self): return self._Tunit @property def TKunit(self): return "K" @property def n(self): return self._nlayer @property def nmesh(self): return self._nmesh @property def nmeshmin(self): return self._nmeshmin @property def resistance(self): return self.l*self.k/self.D @property def permeability(self): return self.D/(self.l*self.k) @property def lag(self): return self.l**2/(6*self.D) @property def pressure(self): return self.k*self.C0 @property def thickness(self): return sum(self.l) @property def concentration(self): return sum(self.l*self.C0)/self.thickness @property def relative_thickness(self): return self.l/self.thickness @property def relative_resistance(self): return self.resistance/sum(self.resistance) @property def rank(self): return (self.n-np.argsort(np.array(self.resistance))).tolist() @property def referencelayer(self): return np.argmax(self.resistance) @property def lreferencelayer(self): return self.l[self.referencelayer] @property def Foscale(self): return self.D[self.referencelayer]/self.lreferencelayer**2 # substance/solute/migrant/chemical (of class migrant or None) @property def substance(self): return self._substance @property def migrant(self): return self.substance # alias/synonym of substance @property def solute(self): return self.substance # alias/synonym of substance @property def chemical(self): return self.substance # alias/synonym of substance # medium (of class foodlayer or None) @property def medium(self): return self._medium # Dmodel and kmodel returned as properties (they are lambda functions) # Note about the implementation: They are attributes that remain None or a callable function # polymer and mass are udpdated on the fly (the code loops over all layers) @property def Dmodel(self): return self._Dmodel @Dmodel.setter def Dmodel(self,value): if value is None or callable(value): self._Dmodel = value else: raise ValueError("Dmodel must be None or a callable function") @property def _compute_Dmodel(self): """Return a callable function that evaluates D with updated parameters.""" if not isinstance(self._substance,migrant) or self._substance.Deval() is None: return lambda **kwargs: None # Return a function that always returns None template = self._substance.Dtemplate.copy() template.update() def func(**kwargs): D = np.empty_like(self._D) for (i,),T in np.ndenumerate(self.T.ravel()): # loop over all layers via T template.update(polymer=self.layerclass_history[i],T=T) # updated layer properties # inherit eventual user parameters D[i] = self._substance.D.evaluate(**dict(template, **kwargs)) return D return func # we return a callable function not a value # polarity index and molar volume are updated on the fly @property def kmodel(self): return self._kmodel @kmodel.setter def kmodel(self,value): if value is None or callable(value): self._kmodel = value else: raise ValueError("kmodel must be None or a callable function") @property def _compute_kmodel(self): """Return a callable function that evaluates k with updated parameters.""" if not isinstance(self._substance,migrant) or self._substance.keval() is None: return lambda **kwargs: None # Return a function that always returns None template = self._substance.ktemplate.copy() # add solute (i) properties: Pi and Vi have been set by loadpubchem already template.update(ispolymer = True) def func(**kwargs): k = np.full_like(self._k,self._k,dtype=np.float64) for (i,),T in np.ndenumerate(self.T.ravel()): # loop over all layers via T if not self.ispolymer_history[i]: # k can be evaluated only in polymes via FH theory continue # we keep the existing k value # add/update monomer properties monomer = migrant(self.chemicalsubstance_history[i]) template.update(Pk = monomer.polarityindex, Vk = monomer.molarvolumeMiller) # inherit eventual user parameters k[i] = self._substance.k.evaluate(**dict(template, **kwargs)) return k return func # we return a callable function not a value @property def hasDmodel(self): """Returns True if a Dmodel has been defined""" if hasattr(self, "_compute_Dmodel"): if self._compute_Dmodel() is not None: return True elif callable(self.Dmodel): return self.Dmodel() is not None return False @property def haskmodel(self): """Returns True if a kmodel has been defined""" if hasattr(self, "_compute_kmodel"): if self._compute_kmodel() is not None: return True elif callable(self.kmodel): return self.kmodel() is not None return False # -------------------------------------------------------------------- # comparators based resistance # -------------------------------------------------------------------- def __eq__(self, o): value1 = self.resistance if self._nlayer>1 else self.resistance[0] if isinstance(o,layer): value2 = o.resistance if o._nlayer>1 else o.resistance[0] else: value2 = o return value1==value2 def __ne__(self, o): value1 = self.resistance if self._nlayer>1 else self.resistance[0] if isinstance(o,layer): value2 = o.resistance if o._nlayer>1 else o.resistance[0] else: value2 = o return value1!=value2 def __lt__(self, o): value1 = self.resistance if self._nlayer>1 else self.resistance[0] if isinstance(o,layer): value2 = o.resistance if o._nlayer>1 else o.resistance[0] else: value2 = o return value1<value2 def __gt__(self, o): value1 = self.resistance if self._nlayer>1 else self.resistance[0] if isinstance(o,layer): value2 = o.resistance if o._nlayer>1 else o.resistance[0] else: value2 = o return value1>value2 def __le__(self, o): value1 = self.resistance if self._nlayer>1 else self.resistance[0] if isinstance(o,layer): value2 = o.resistance if o._nlayer>1 else o.resistance[0] else: value2 = o return value1<=value2 def __ge__(self, o): value1 = self.resistance if self._nlayer>1 else self.resistance[0] if isinstance(o,layer): value2 = o.resistance if o._nlayer>1 else o.resistance[0] else: value2 = o return value1>=value2 # -------------------------------------------------------------------- # Generates mesh # -------------------------------------------------------------------- def mesh(self,nmesh=None,nmeshmin=None): """ nmesh() generates mesh based on nmesh and nmeshmin, nmesh(nmesh=value,nmeshmin=value) """ if nmesh==None: nmesh = self.nmesh if nmeshmin==None: nmeshmin = self.nmeshmin if nmeshmin>nmesh: nmeshmin,nmesh = nmesh, nmeshmin # X = mesh distribution (number of nodes per layer) X = np.ones(self._nlayer) for i in range(1,self._nlayer): X[i] = X[i-1]*(self.permeability[i-1]*self.l[i])/(self.permeability[i]*self.l[i-1]) X = np.maximum(nmeshmin,np.ceil(nmesh*X/sum(X))) X = np.round((X/sum(X))*nmesh).astype(int) # do the mesh x0 = 0 mymesh = [] for i in range(self._nlayer): mymesh.append(mesh(self.l[i]/self.l[self.referencelayer],X[i],x0=x0,index=i)) x0 += self.l[i] return mymesh # -------------------------------------------------------------------- # Setter methods and tools to validate inputs checknumvalue and checktextvalue # -------------------------------------------------------------------- @physicalstate.setter def physicalstate(self,value): if value not in ("solid","liquid","gas","supercritical"): raise ValueError(f"physicalstate must be solid/liduid/gas/supercritical and not {value}") self._physicalstate = value @chemicalclass.setter def chemicalclass(self,value): if value not in ("polymer","other"): raise ValueError(f"chemicalclass must be polymer/oher and not {value}") self._chemicalclass= value @chemicalsubstance.setter def chemicalsubstance(self,value): if not isinstance(value,str): raise ValueError("chemicalsubtance must be str not a {type(value).__name__}") self._chemicalsubstance= value @polarityindex.setter def polarityindex(self,value): if not isinstance(value,(float,int)): raise ValueError("polarity index must be float not a {type(value).__name__}") self._polarityindex= value def checknumvalue(self,value,ExpectedUnits=None): """ returns a validate value to set properties """ if isinstance(value,tuple): value = check_units(value,ExpectedUnits=ExpectedUnits) if isinstance(value,int): value = float(value) if isinstance(value,float): value = np.array([value]) if isinstance(value,list): value = np.array(value) if len(value)>self._nlayer: value = value[:self._nlayer] if self.verbosity>1 and self.verbose: print('dimension mismatch, the extra value(s) has been removed') elif len(value)<self._nlayer: value = np.concatenate((value,value[-1:]*np.ones(self._nlayer-len(value)))) if self.verbosity>1 and self.verbose: print('dimension mismatch, the last value has been repeated') return value def checktextvalue(self,value): """ returns a validate value to set properties """ if not isinstance(value,list): value = [value] if len(value)>self._nlayer: value = value[:self._nlayer] if self.verbosity>1 and self.verbose: print('dimension mismatch, the extra entry(ies) has been removed') elif len(value)<self._nlayer: value = value + value[-1:]*(self._nlayer-len(value)) if self.verbosity>1 and self.verbose: print('dimension mismatch, the last entry has been repeated') return value @l.setter def l(self,value): self._l =self.checknumvalue(value,layer._defaults["lunit"]) @D.setter def D(self,value): self._D=self.checknumvalue(value,layer._defaults["Dunit"]) @k.setter def k(self,value): self._k =self.checknumvalue(value,layer._defaults["kunit"]) @C0.setter def C0(self,value): self._C0 =self.checknumvalue(value,layer._defaults["Cunit"]) @rho.setter def rho(self,value): self._rho =self.checknumvalue(value,layer._defaults["rhounit"]) @T.setter def T(self,value): self._T =self.checknumvalue(value,layer._defaults["Tunit"]) @name.setter def name(self,value): self._name =self.checktextvalue(value) @type.setter def type(self,value): self._type =self.checktextvalue(value) @material.setter def material(self,value): self._material =self.checktextvalue(value) @nmesh.setter def nmesh(self,value): self._nmesh = max(value,self._nlayer*self._nmeshmin) @nmeshmin.setter def nmeshmin(self,value): self._nmeshmin = max(value,round(self._nmesh/(2*self._nlayer))) @substance.setter def substance(self,value): if isinstance(value,str): value = migrant(value) if not isinstance(value,migrant) and value is not None: raise TypeError(f"value must be a migrant not a {type(value).__name__}") self._substance = value @migrant.setter def migrant(self,value): self.substance = value @chemical.setter def chemical(self,value): self.substance = value @solute.setter def solute(self,value): self.substance = value @medium.setter def medium(self,value): from patankar.food import foodlayer if not isinstance(value,foodlayer): raise TypeError(f"value must be a foodlayer not a {type(value).__name__}") self._medium = value # -------------------------------------------------------------------- # getter and setter for links: Dlink, klink, C0link, Tlink, llink # -------------------------------------------------------------------- @property def Dlink(self): """Getter for Dlink""" return self._Dlink @Dlink.setter def Dlink(self, value): """Setter for Dlink""" self._Dlink = self._initialize_link(value, "D") if isinstance(value,layerLink): value._maxlength = self.n @property def klink(self): """Getter for klink""" return self._klink @klink.setter def klink(self, value): """Setter for klink""" self._klink = self._initialize_link(value, "k") if isinstance(value,layerLink): value._maxlength = self.n @property def C0link(self): """Getter for C0link""" return self._C0link @C0link.setter def C0link(self, value): """Setter for C0link""" self._C0link = self._initialize_link(value, "C0") if isinstance(value,layerLink): value._maxlength = self.n @property def Tlink(self): """Getter for Tlink""" return self._Tlink @Tlink.setter def Tlink(self, value): """Setter for Tlink""" self._Tlink = self._initialize_link(value, "T") if isinstance(value,layerLink): value._maxlength = self.n @property def llink(self): """Getter for llink""" return self._llink @llink.setter def llink(self, value): """Setter for llink""" self._llink = self._initialize_link(value, "l") if isinstance(value,layerLink): value._maxlength = self.n @property def hasDlink(self): """Returns True if Dlink is defined""" return self.Dlink is not None @property def hasklink(self): """Returns True if klink is defined""" return self.klink is not None @property def hasC0link(self): """Returns True if C0link is defined""" return self.C0link is not None @property def hasTlink(self): """Returns True if Tlink is defined""" return self.Tlink is not None @property def hasllink(self): """Returns True if llink is defined""" return self.llink is not None # -------------------------------------------------------------------- # returned LaTeX-formated properties # -------------------------------------------------------------------- def Dlatex(self, numdigits=4, units=r"\mathrm{m^2 \cdot s^{-1}}",prefix="D=",mathmode="$"): """Returns diffusivity values (D) formatted in LaTeX scientific notation.""" return [format_scientific_latex(D, numdigits, units, prefix,mathmode) for D in self.D] def klatex(self, numdigits=4, units="a.u.",prefix="k=",mathmode="$"): """Returns Henry-like values (k) formatted in LaTeX scientific notation.""" return [format_scientific_latex(k, numdigits, units, prefix,mathmode) for k in self.k] def llatex(self, numdigits=4, units="m",prefix="l=",mathmode="$"): """Returns thickness values (k) formatted in LaTeX scientific notation.""" return [format_scientific_latex(l, numdigits, units, prefix,mathmode) for l in self.l] def C0latex(self, numdigits=4, units="a.u.",prefix="C0=",mathmode="$"): """Returns Initial Concentratoin values (C0) formatted in LaTeX scientific notation.""" return [format_scientific_latex(c, numdigits, units, prefix,mathmode) for c in self.C0] # -------------------------------------------------------------------- # hash methods (assembly and layer-by-layer) # note that list needs to be converted into tuples to be hashed # -------------------------------------------------------------------- def __hash__(self): """ hash layer-object (assembly) method """ return hash((tuple(self._name), tuple(self._type), tuple(self._material), tuple(self._l), tuple(self._D), tuple(self.k), tuple(self._C0), tuple(self._rho))) # layer-by-layer @property = decoration to consider it # as a property instead of a method/attribute # comprehension for n in range(self._nlayer) applies it to all layers @property def hashlayer(self): """ hash layer (layer-by-layer) method """ return [hash((self._name[n], self._type[n], self._material[n], self._l[n], self._D[n], self.k[n], self._C0[n], self._rho[n])) for n in range(self._nlayer) ] # -------------------------------------------------------------------- # repr method (since the getter are defined, the '_' is dropped) # -------------------------------------------------------------------- # density and temperature are not shown def __repr__(self): """ disp method """ print("\n[%s version=%0.4g, contact=%s]" % (self.__description,self.__version,self.__contact)) if self._nlayer==0: print("empty %s" % (self.__description)) else: hasDmodel, haskmodel = self.hasDmodel, self.haskmodel hasDlink, hasklink, hasC0link, hasTlink, hasllink = self.hasDlink, self.hasklink, self.hasC0link, self.hasTlink, self.hasllink properties_hasmodel = {"l":False,"D":hasDmodel,"k":haskmodel,"C0":False} properties_haslink = {"l":hasllink,"D":hasDlink,"k":hasklink,"C0":hasC0link,"T":hasTlink} if hasDmodel or haskmodel: properties_hasmodel["T"] = False fmtval = '%10s: '+self._printformat+" [%s]" fmtstr = '%10s= %s' if self._nlayer==1: print(f'monolayer of {self.__description}:') else: print(f'{self._nlayer}-multilayer of {self.__description}:') for n in range(1,self._nlayer+1): modelinfo = { "D": f"{self._substance.D.__name__}({self.layerclass_history[n-1]},{self._substance},T={float(self.T[0])} {self.Tunit})" if hasDmodel else "", "k": f"{self._substance.k.__name__}(<{self.chemicalsubstance_history[n-1]}>,{self._substance})" if haskmodel else "", } print('-- [ layer %d of %d ] ---------- barrier rank=%d --------------' % (n,self._nlayer,self.rank[n-1])) for p in ["name","type","material","code"]: v = getattr(self,p) print('%10s: "%s"' % (p,v[n-1]),flush=True) for p in properties_hasmodel.keys(): v = getattr(self,p) # value vunit = getattr(self,p[0]+"unit") # value unit print(fmtval % (p,v[n-1],vunit),flush=True) isoverridenbylink = False if properties_haslink[p]: isoverridenbylink = not np.isnan(getattr(self,p+"link").get(n-1)) if isoverridenbylink: print(fmtstr % ("",f"value controlled by {p}link[{n-1}] (external)"),flush=True) elif properties_hasmodel[p]: print(fmtstr % ("",modelinfo[p]),flush=True) return str(self) def __str__(self): """Formatted string representation of layer""" all_identical = len(set(self.layerclass_history)) == 1 cls = self.__class__.__name__ if all_identical else "multilayer" return f"<{cls} with {self.n} layer{'s' if self.n>1 else ''}: {self.name}>" # -------------------------------------------------------------------- # Returns the equivalent dictionary from an object for debugging # -------------------------------------------------------------------- def _todict(self): """ returns the equivalent dictionary from an object """ return dict((key, getattr(self, key)) for key in dir(self) if key not in dir(self.__class__)) # -------------------------------------------------------------------- # -------------------------------------------------------------------- # Simplify layers by collecting similar ones # -------------------------------------------------------------------- def simplify(self): """ merge continuous layers of the same type """ nlayer = self._nlayer if nlayer>1: res = self[0] ires = 0 ireshash = res.hashlayer[0] for i in range(1,nlayer): if self.hashlayer[i]==ireshash: res.l[ires] = res.l[ires]+self.l[i] else: res = res + self[i] ires = ires+1 ireshash = self.hashlayer[i] else: res = self.copy() return res # -------------------------------------------------------------------- # Split layers into a tuple # -------------------------------------------------------------------- def split(self): """ split layers """ out = () if self._nlayer>0: for i in range(self._nlayer): out = out + (self[i],) # (,) special syntax for tuple singleton return out # -------------------------------------------------------------------- # deepcopy # -------------------------------------------------------------------- def copy(self,**kwargs): """ Creates a deep copy of the current layer instance. Returns: - layer: A new layer instance identical to the original. """ return duplicate(self).update(**kwargs) # -------------------------------------------------------------------- # update contact conditions from a foodphysics instance (or do the reverse) # material << medium # material@medium # -------------------------------------------------------------------- def _from(self,medium=None): """Propagates contact conditions from food instance""" from patankar.food import foodphysics, foodlayer if not isinstance(medium,foodphysics): raise TypeError(f"medium must be a foodphysics, foodlayer not a {type(medium).__name__}") if not hasattr(medium, "contacttemperature"): medium.contacttemperature = self.T[0] T = medium.get_param("contacttemperature",40,acceptNone=False) self.T = np.full_like(self.T,T,dtype=np.float64) if medium.substance is not None: self.substance = medium.substance else: medium.substance = self.substance # do the reverse if substance is not defined in medium # inherit fully medium only if it is a foodlayer (foodphysics is too restrictive) if isinstance(medium,foodlayer): self.medium = medium # overload operator << def __lshift__(self, medium): """Overloads << to propagate contact conditions from food.""" self._from(medium) # overload operator @ (same as <<) def __matmul__(self, medium): """Overloads @ to propagate contact conditions from food.""" self._from(medium) # -------------------------------------------------------------------- # Inheritance registration mechanism associated with food >> layer # It is used by food, not by layer (please refer to food.py). # Note that layer >> food means mass transfer simulation # -------------------------------------------------------------------- def acknowledge(self, what=None, category=None): """ Register inherited properties under a given category. Parameters: ----------- what : str or list of str or a set The properties or attributes that have been inherited. category : str The category under which the properties are grouped. """ if category is None or what is None: raise ValueError("Both 'what' and 'category' must be provided.") if isinstance(what, str): what = {what} # Convert string to a set elif isinstance(what, list): what = set(what) # Convert list to a set for uniqueness elif not isinstance(what,set): raise TypeError("'what' must be a string, a list, or a set of strings.") if category not in self._hasbeeninherited: self._hasbeeninherited[category] = set() self._hasbeeninherited[category].update(what) # -------------------------------------------------------------------- # migration simulation overloaded as sim = layer >> food # using layer >> food without output works also. # The result is stored in food.lastsimulation # -------------------------------------------------------------------- def contact(self,medium,**kwargs): """alias to migration method""" return self.migration(medium,**kwargs) def migration(self,medium=None,**kwargs): """interface to simulation engine: senspantankar""" from patankar.food import foodphysics from patankar.migration import senspatankar if medium is None: medium = self.medium if not isinstance(medium,foodphysics): raise TypeError(f"medium must be a foodphysics not a {type(medium).__name__}") sim = senspatankar(self,medium,**kwargs) medium.lastsimulation = sim # store the last simulation result in medium medium.lastinput = self # store the last input (self) sim.savestate(self,medium) # store store the inputs in sim for chaining return sim # overloading operation def __rshift__(self, medium): """Overloads >> to propagate migration to food.""" from patankar.food import foodphysics if not isinstance(medium,foodphysics): raise TypeError(f"medium must be a foodphysics object not a {type(medium).__name__}") return self.contact(medium) # -------------------------------------------------------------------- # Safe update method # -------------------------------------------------------------------- def update(self, **kwargs): """ Update layer parameters following strict validation rules. Rules: 1) key should be listed in self._defaults 2) for some keys, synonyms are acceptable as reported in self._synonyms 3) values cannot be None if they were not None in _defaults 4) values should be str if they were initially str, idem with bool 5) values which were numeric (int, float, np.ndarray) should remain numeric. 6) lists are acceptable as numeric arrays 7) all numerical (float, np.ndarray, list) except int must be converted into numpy arrays. Values which were int in _defaults must remain int and an error should be raised if a float value is proposed. 8) keys listed in _parametersWithUnits can be assigned with tuples (value, "unit"). They will be converted automatically with check_units(value). 9) for parameters with a default value None, any value is acceptable 10) A clear error message should be displayed for any bad value showing the current value of the parameter and its default value. """ if not kwargs: # shortcut return self # for chaining param_counts = {key: 0 for key in self._defaults} # Track how many times each param is set def resolve_key(key): """Resolve key considering synonyms and check for duplicates.""" for main_key, synonyms in self._synonyms.items(): if key == main_key or key in synonyms: param_counts[main_key] += 1 return main_key param_counts[key] += 1 return key def validate_value(key, value): """Validate and process the value according to the rules.""" default_value = self._defaults[key] # Rule 3: values cannot be None if they were not None in _defaults if value is None and default_value is not None: raise ValueError(f"Invalid value for '{key}': None is not allowed. " f"Current: {getattr(self, key)}, Default: {default_value}") # Rule 9: If default is None, any value is acceptable if default_value is None: return value # Rule 4 & 5: Ensure type consistency (str, bool, or numeric types) if isinstance(default_value, str) and not isinstance(value, str): raise TypeError(f"Invalid type for '{key}': Expected str, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") if isinstance(default_value, bool) and not isinstance(value, bool): raise TypeError(f"Invalid type for '{key}': Expected bool, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") # Rule 6 & 7: Convert numeric types properly if isinstance(default_value, (int, float, np.ndarray)): if isinstance(value, list): value = np.array(value) if isinstance(default_value, int): if isinstance(value, float) or (isinstance(value, np.ndarray) and np.issubdtype(value.dtype, np.floating)): raise TypeError(f"Invalid type for '{key}': Expected integer, got float. " f"Current: {getattr(self, key)}, Default: {default_value}") if isinstance(value, (int, np.integer)): return int(value) # Ensure it remains an int raise TypeError(f"Invalid type for '{key}': Expected integer, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") if isinstance(value, (int, float, list, np.ndarray)): return np.array(value, dtype=float) # Convert everything to np.array for floats raise TypeError(f"Invalid type for '{key}': Expected numeric, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") # Rule 8: Convert units if applicable if key in self._parametersWithUnits and isinstance(value, tuple): value, unit = value converted_value, _ = check_units((value, unit), ExpectedUnits=self._parametersWithUnits[key]) return converted_value return value # Apply updates while tracking parameter occurrences for key, value in kwargs.items(): resolved_key = resolve_key(key) if resolved_key not in self._defaults: raise KeyError(f"Invalid key '{key}'. Allowed keys: {list(self._defaults.keys())}.") try: validated_value = validate_value(resolved_key, value) setattr(self, resolved_key, validated_value) except (TypeError, ValueError) as e: raise ValueError(f"Error updating '{key}': {e}") # Ensure that no parameter was set multiple times due to synonyms duplicate_keys = [k for k, v in param_counts.items() if v > 1] if duplicate_keys: raise ValueError(f"Duplicate assignment detected for parameters: {duplicate_keys}. " "Use only one synonym per parameter.") return self # to enable chaining # Basic tool for debugging # -------------------------------------------------------------------- # STRUCT method - returns the equivalent dictionary from an object # -------------------------------------------------------------------- def struct(self): """ returns the equivalent dictionary from an object """ return dict((key, getattr(self, key)) for key in dir(self) if key not in dir(self.__class__))
Subclasses
- AdhesiveAcrylate
- AdhesiveEVA
- AdhesiveNaturalRubber
- AdhesivePU
- AdhesivePVAC
- AdhesiveSyntheticRubber
- AdhesiveVAE
- Cardboard
- HDPE
- HIPS
- LDPE
- LLDPE
- PA6
- PA66
- PBT
- PEN
- PP
- PPrubber
- PS
- Paper
- SBS
- air
- gPET
- oPP
- plasticizedPVC
- rPET
- rigidPVC
Static methods
def help()
-
Prints a dynamically formatted summary of all input parameters, adjusting column widths based on content and wrapping long descriptions.
Expand source code
@classmethod def help(cls): """ Prints a dynamically formatted summary of all input parameters, adjusting column widths based on content and wrapping long descriptions. """ # Column Headers headers = ["Parameter", "Default Value", "Has Synonyms?", "Description"] col_widths = [len(h) for h in headers] # Start with header widths # Collect Data Rows rows = [] for param, default in cls._defaults.items(): has_synonyms = "✅ Yes" if param in cls._synonyms else "❌ No" description = cls._descriptionInputs.get(param, "No description available") # Update column widths dynamically col_widths[0] = max(col_widths[0], len(param)) col_widths[1] = max(col_widths[1], len(str(default))) col_widths[2] = max(col_widths[2], len(has_synonyms)) col_widths[3] = max(col_widths[3], len(description)) rows.append([param, str(default), has_synonyms, description]) # Function to wrap text for a given column width def wrap_text(text, width): return textwrap.fill(text, width) # Print Table with Adjusted Column Widths separator = "+-" + "-+-".join("-" * w for w in col_widths) + "-+" print("\n### **Accepted Parameters and Defaults**\n") print(separator) print("| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + " |") print(separator) for row in rows: # Wrap text in the description column row[3] = wrap_text(row[3], col_widths[3]) # Print row print("| " + " | ".join(row[i].ljust(col_widths[i]) for i in range(3)) + " | " + row[3]) print(separator) # Synonyms Table print("\n### **Parameter Synonyms**\n") syn_headers = ["Parameter", "Synonyms"] syn_col_widths = [ max(len("Parameter"), max(len(k) for k in cls._synonyms.keys())), # Ensure it fits "Parameter" max(len("Synonyms"), max(len(", ".join(v)) for v in cls._synonyms.values())) # Ensure it fits "Synonyms" ] syn_separator = "+-" + "-+-".join("-" * w for w in syn_col_widths) + "-+" print(syn_separator) print("| " + " | ".join(h.ljust(syn_col_widths[i]) for i, h in enumerate(syn_headers)) + " |") print(syn_separator) for param, synonyms in cls._synonyms.items(): print(f"| {param.ljust(syn_col_widths[0])} | {', '.join(synonyms).ljust(syn_col_widths[1])} |") print(syn_separator)
def resolvename(param_value, param_key, **unresolved)
-
Resolves the correct parameter value using known synonyms.
- If param_value is already set (not None), return it.
- If a synonym exists in **unresolved, assign its value.
- If multiple synonyms of the same parameter appear in **unresolved, raise an error.
- Otherwise, return None.
Parameters: -
param_name
(any): The original value (if provided). -param_key
(str): The legitimate parameter name we are resolving. -unresolved
(dict): The dictionary of unrecognized keyword arguments.Returns: - The resolved value or None if not found.
Expand source code
@classmethod def resolvename(cls, param_value, param_key, **unresolved): """ Resolves the correct parameter value using known synonyms. - If param_value is already set (not None), return it. - If a synonym exists in **unresolved, assign its value. - If multiple synonyms of the same parameter appear in **unresolved, raise an error. - Otherwise, return None. Parameters: - `param_name` (any): The original value (if provided). - `param_key` (str): The legitimate parameter name we are resolving. - `unresolved` (dict): The dictionary of unrecognized keyword arguments. Returns: - The resolved value or None if not found. """ if param_value is not None: return param_value # The parameter is explicitly defined, do not override if not unresolved: # shortcut return None resolved_value = None found_keys = [] # Check if param_key itself is present in unresolved if param_key in unresolved: found_keys.append(param_key) resolved_value = unresolved[param_key] # Check if any of its synonyms are in unresolved if param_key in cls._synonyms: for synonym in cls._synonyms[param_key]: if synonym in unresolved: found_keys.append(synonym) resolved_value = unresolved[synonym] # Raise error if multiple synonyms were found if len(found_keys) > 1: raise ValueError( f"Conflicting definitions: Multiple synonyms {found_keys} were provided for '{param_key}'." ) return resolved_value
Instance variables
var C0
-
Expand source code
@property def C0(self): return self._C0 if not self.hasC0link else self.COlink.getfull(self._C0)
var C0link
-
Getter for C0link
Expand source code
@property def C0link(self): """Getter for C0link""" return self._C0link
var Cunit
-
Expand source code
@property def Cunit(self): return self._Cunit
var D
-
Expand source code
@property def D(self): Dtmp = None if self.Dmodel == "default": # default behavior Dtmp = self._compute_Dmodel() elif callable(self.Dmodel): # user override Dtmp = self.Dmodel() if Dtmp is not None: Dtmp = np.full_like(self._D, Dtmp,dtype=np.float64) if self.hasDlink: return self.Dlink.getfull(Dtmp) # substitution rules are applied as defined in Dlink else: return Dtmp return self._D if not self.hasDlink else self.Dlink.getfull(self._D)
var Dlink
-
Getter for Dlink
Expand source code
@property def Dlink(self): """Getter for Dlink""" return self._Dlink
var Dmodel
-
Expand source code
@property def Dmodel(self): return self._Dmodel
var Dunit
-
Expand source code
@property def Dunit(self): return self._Dunit
var Foscale
-
Expand source code
@property def Foscale(self): return self.D[self.referencelayer]/self.lreferencelayer**2
var T
-
Expand source code
@property def T(self): return self._T if not self.hasTlink else self.Tlink.getfull(self._T)
var TK
-
Expand source code
@property def TK(self): return self._T+T0K
var TKunit
-
Expand source code
@property def TKunit(self): return "K"
var Tlink
-
Getter for Tlink
Expand source code
@property def Tlink(self): """Getter for Tlink""" return self._Tlink
var Tunit
-
Expand source code
@property def Tunit(self): return self._Tunit
var chemical
-
Expand source code
@property def chemical(self): return self.substance # alias/synonym of substance
var chemicalclass
-
Expand source code
@property def chemicalclass(self): return self._chemicalclass
var chemicalsubstance
-
Expand source code
@property def chemicalsubstance(self): return self._chemicalsubstance
var chemicalsubstance_history
-
Expand source code
@property def chemicalsubstance_history(self): return self._chemicalsubstance_history if self._chemicalsubstance_history != [] else [self.chemicalsubstance]
var code
-
Expand source code
@property def code(self): return self._code
var concentration
-
Expand source code
@property def concentration(self): return sum(self.l*self.C0)/self.thickness
var hasC0link
-
Returns True if C0link is defined
Expand source code
@property def hasC0link(self): """Returns True if C0link is defined""" return self.C0link is not None
var hasDlink
-
Returns True if Dlink is defined
Expand source code
@property def hasDlink(self): """Returns True if Dlink is defined""" return self.Dlink is not None
var hasDmodel
-
Returns True if a Dmodel has been defined
Expand source code
@property def hasDmodel(self): """Returns True if a Dmodel has been defined""" if hasattr(self, "_compute_Dmodel"): if self._compute_Dmodel() is not None: return True elif callable(self.Dmodel): return self.Dmodel() is not None return False
var hasTlink
-
Returns True if Tlink is defined
Expand source code
@property def hasTlink(self): """Returns True if Tlink is defined""" return self.Tlink is not None
var hashlayer
-
hash layer (layer-by-layer) method
Expand source code
@property def hashlayer(self): """ hash layer (layer-by-layer) method """ return [hash((self._name[n], self._type[n], self._material[n], self._l[n], self._D[n], self.k[n], self._C0[n], self._rho[n])) for n in range(self._nlayer) ]
var hasklink
-
Returns True if klink is defined
Expand source code
@property def hasklink(self): """Returns True if klink is defined""" return self.klink is not None
var haskmodel
-
Returns True if a kmodel has been defined
Expand source code
@property def haskmodel(self): """Returns True if a kmodel has been defined""" if hasattr(self, "_compute_kmodel"): if self._compute_kmodel() is not None: return True elif callable(self.kmodel): return self.kmodel() is not None return False
var hasllink
-
Returns True if llink is defined
Expand source code
@property def hasllink(self): """Returns True if llink is defined""" return self.llink is not None
var ispolymer
-
Expand source code
@property def ispolymer(self): return self.chemicalclass == "polymer"
var ispolymer_history
-
Expand source code
@property def ispolymer_history(self): return self._ispolymer_history if self._ispolymer_history != [] else [self.ispolymer]
var issolid
-
Expand source code
@property def issolid(self): return self.physicalstate == "solid"
var k
-
Expand source code
@property def k(self): ktmp = None if self.kmodel == "default": # default behavior ktmp = self._compute_kmodel() elif callable(self.kmodel): # user override ktmp = self.kmodel() if ktmp is not None: ktmp = np.full_like(self._k, ktmp,dtype=np.float64) if self.hasklink: return self.klink.getfull(ktmp) # substitution rules are applied as defined in klink else: return ktmp return self._k if not self.hasklink else self.klink.getfull(self._k)
var klink
-
Getter for klink
Expand source code
@property def klink(self): """Getter for klink""" return self._klink
var kmodel
-
Expand source code
@property def kmodel(self): return self._kmodel
var kunit
-
Expand source code
@property def kunit(self): return self._kunit
var l
-
Expand source code
@property def l(self): return self._l if not self.hasllink else self.llink.getfull(self._l)
var lag
-
Expand source code
@property def lag(self): return self.l**2/(6*self.D)
var layerclass
-
Expand source code
@property def layerclass(self): return type(self).__name__
var layerclass_history
-
Expand source code
@property def layerclass_history(self): return self._layerclass_history if self._layerclass_history != [] else [self.layerclass]
var llink
-
Getter for llink
Expand source code
@property def llink(self): """Getter for llink""" return self._llink
var lreferencelayer
-
Expand source code
@property def lreferencelayer(self): return self.l[self.referencelayer]
var lunit
-
Expand source code
@property def lunit(self): return self._lunit
var material
-
Expand source code
@property def material(self): return self._material
var medium
-
Expand source code
@property def medium(self): return self._medium
var migrant
-
Expand source code
@property def migrant(self): return self.substance # alias/synonym of substance
var n
-
Expand source code
@property def n(self): return self._nlayer
var name
-
Expand source code
@property def name(self): return self._name
var nmesh
-
Expand source code
@property def nmesh(self): return self._nmesh
var nmeshmin
-
Expand source code
@property def nmeshmin(self): return self._nmeshmin
var permeability
-
Expand source code
@property def permeability(self): return self.D/(self.l*self.k)
var physicalstate
-
Expand source code
@property def physicalstate(self): return self._physicalstate
var polarityindex
-
Expand source code
@property def polarityindex(self): # rescaled to match predictions - standard scale [0,10.2] - predicted scale [0,7.12] return self._polarityindex * migrant("water").polarityindex/10.2
var pressure
-
Expand source code
@property def pressure(self): return self.k*self.C0
var rank
-
Expand source code
@property def rank(self): return (self.n-np.argsort(np.array(self.resistance))).tolist()
var referencelayer
-
Expand source code
@property def referencelayer(self): return np.argmax(self.resistance)
var relative_resistance
-
Expand source code
@property def relative_resistance(self): return self.resistance/sum(self.resistance)
var relative_thickness
-
Expand source code
@property def relative_thickness(self): return self.l/self.thickness
var resistance
-
Expand source code
@property def resistance(self): return self.l*self.k/self.D
var rho
-
Expand source code
@property def rho(self): return self._rho
var rhounit
-
Expand source code
@property def rhounit(self): return self._rhounit
var solute
-
Expand source code
@property def solute(self): return self.substance # alias/synonym of substance
var substance
-
Expand source code
@property def substance(self): return self._substance
var thickness
-
Expand source code
@property def thickness(self): return sum(self.l)
var type
-
Expand source code
@property def type(self): return self._type
Methods
def C0latex(self, numdigits=4, units='a.u.', prefix='C0=', mathmode='$')
-
Returns Initial Concentratoin values (C0) formatted in LaTeX scientific notation.
Expand source code
def C0latex(self, numdigits=4, units="a.u.",prefix="C0=",mathmode="$"): """Returns Initial Concentratoin values (C0) formatted in LaTeX scientific notation.""" return [format_scientific_latex(c, numdigits, units, prefix,mathmode) for c in self.C0]
def Dlatex(self, numdigits=4, units='\\mathrm{m^2 \\cdot s^{-1}}', prefix='D=', mathmode='$')
-
Returns diffusivity values (D) formatted in LaTeX scientific notation.
Expand source code
def Dlatex(self, numdigits=4, units=r"\mathrm{m^2 \cdot s^{-1}}",prefix="D=",mathmode="$"): """Returns diffusivity values (D) formatted in LaTeX scientific notation.""" return [format_scientific_latex(D, numdigits, units, prefix,mathmode) for D in self.D]
def acknowledge(self, what=None, category=None)
-
Register inherited properties under a given category.
Parameters:
what : str or list of str or a set The properties or attributes that have been inherited. category : str The category under which the properties are grouped.
Expand source code
def acknowledge(self, what=None, category=None): """ Register inherited properties under a given category. Parameters: ----------- what : str or list of str or a set The properties or attributes that have been inherited. category : str The category under which the properties are grouped. """ if category is None or what is None: raise ValueError("Both 'what' and 'category' must be provided.") if isinstance(what, str): what = {what} # Convert string to a set elif isinstance(what, list): what = set(what) # Convert list to a set for uniqueness elif not isinstance(what,set): raise TypeError("'what' must be a string, a list, or a set of strings.") if category not in self._hasbeeninherited: self._hasbeeninherited[category] = set() self._hasbeeninherited[category].update(what)
def checknumvalue(self, value, ExpectedUnits=None)
-
returns a validate value to set properties
Expand source code
def checknumvalue(self,value,ExpectedUnits=None): """ returns a validate value to set properties """ if isinstance(value,tuple): value = check_units(value,ExpectedUnits=ExpectedUnits) if isinstance(value,int): value = float(value) if isinstance(value,float): value = np.array([value]) if isinstance(value,list): value = np.array(value) if len(value)>self._nlayer: value = value[:self._nlayer] if self.verbosity>1 and self.verbose: print('dimension mismatch, the extra value(s) has been removed') elif len(value)<self._nlayer: value = np.concatenate((value,value[-1:]*np.ones(self._nlayer-len(value)))) if self.verbosity>1 and self.verbose: print('dimension mismatch, the last value has been repeated') return value
def checktextvalue(self, value)
-
returns a validate value to set properties
Expand source code
def checktextvalue(self,value): """ returns a validate value to set properties """ if not isinstance(value,list): value = [value] if len(value)>self._nlayer: value = value[:self._nlayer] if self.verbosity>1 and self.verbose: print('dimension mismatch, the extra entry(ies) has been removed') elif len(value)<self._nlayer: value = value + value[-1:]*(self._nlayer-len(value)) if self.verbosity>1 and self.verbose: print('dimension mismatch, the last entry has been repeated') return value
def contact(self, medium, **kwargs)
-
alias to migration method
Expand source code
def contact(self,medium,**kwargs): """alias to migration method""" return self.migration(medium,**kwargs)
def copy(self, **kwargs)
-
Creates a deep copy of the current layer instance.
Returns: - layer: A new layer instance identical to the original.
Expand source code
def copy(self,**kwargs): """ Creates a deep copy of the current layer instance. Returns: - layer: A new layer instance identical to the original. """ return duplicate(self).update(**kwargs)
def klatex(self, numdigits=4, units='a.u.', prefix='k=', mathmode='$')
-
Returns Henry-like values (k) formatted in LaTeX scientific notation.
Expand source code
def klatex(self, numdigits=4, units="a.u.",prefix="k=",mathmode="$"): """Returns Henry-like values (k) formatted in LaTeX scientific notation.""" return [format_scientific_latex(k, numdigits, units, prefix,mathmode) for k in self.k]
def llatex(self, numdigits=4, units='m', prefix='l=', mathmode='$')
-
Returns thickness values (k) formatted in LaTeX scientific notation.
Expand source code
def llatex(self, numdigits=4, units="m",prefix="l=",mathmode="$"): """Returns thickness values (k) formatted in LaTeX scientific notation.""" return [format_scientific_latex(l, numdigits, units, prefix,mathmode) for l in self.l]
def mesh(self, nmesh=None, nmeshmin=None)
-
nmesh() generates mesh based on nmesh and nmeshmin, nmesh(nmesh=value,nmeshmin=value)
Expand source code
def mesh(self,nmesh=None,nmeshmin=None): """ nmesh() generates mesh based on nmesh and nmeshmin, nmesh(nmesh=value,nmeshmin=value) """ if nmesh==None: nmesh = self.nmesh if nmeshmin==None: nmeshmin = self.nmeshmin if nmeshmin>nmesh: nmeshmin,nmesh = nmesh, nmeshmin # X = mesh distribution (number of nodes per layer) X = np.ones(self._nlayer) for i in range(1,self._nlayer): X[i] = X[i-1]*(self.permeability[i-1]*self.l[i])/(self.permeability[i]*self.l[i-1]) X = np.maximum(nmeshmin,np.ceil(nmesh*X/sum(X))) X = np.round((X/sum(X))*nmesh).astype(int) # do the mesh x0 = 0 mymesh = [] for i in range(self._nlayer): mymesh.append(mesh(self.l[i]/self.l[self.referencelayer],X[i],x0=x0,index=i)) x0 += self.l[i] return mymesh
def migration(self, medium=None, **kwargs)
-
interface to simulation engine: senspantankar
Expand source code
def migration(self,medium=None,**kwargs): """interface to simulation engine: senspantankar""" from patankar.food import foodphysics from patankar.migration import senspatankar if medium is None: medium = self.medium if not isinstance(medium,foodphysics): raise TypeError(f"medium must be a foodphysics not a {type(medium).__name__}") sim = senspatankar(self,medium,**kwargs) medium.lastsimulation = sim # store the last simulation result in medium medium.lastinput = self # store the last input (self) sim.savestate(self,medium) # store store the inputs in sim for chaining return sim
def simplify(self)
-
merge continuous layers of the same type
Expand source code
def simplify(self): """ merge continuous layers of the same type """ nlayer = self._nlayer if nlayer>1: res = self[0] ires = 0 ireshash = res.hashlayer[0] for i in range(1,nlayer): if self.hashlayer[i]==ireshash: res.l[ires] = res.l[ires]+self.l[i] else: res = res + self[i] ires = ires+1 ireshash = self.hashlayer[i] else: res = self.copy() return res
def split(self)
-
split layers
Expand source code
def split(self): """ split layers """ out = () if self._nlayer>0: for i in range(self._nlayer): out = out + (self[i],) # (,) special syntax for tuple singleton return out
def struct(self)
-
returns the equivalent dictionary from an object
Expand source code
def struct(self): """ returns the equivalent dictionary from an object """ return dict((key, getattr(self, key)) for key in dir(self) if key not in dir(self.__class__))
def update(self, **kwargs)
-
Update layer parameters following strict validation rules.
Rules: 1) key should be listed in self._defaults 2) for some keys, synonyms are acceptable as reported in self._synonyms 3) values cannot be None if they were not None in _defaults 4) values should be str if they were initially str, idem with bool 5) values which were numeric (int, float, np.ndarray) should remain numeric. 6) lists are acceptable as numeric arrays 7) all numerical (float, np.ndarray, list) except int must be converted into numpy arrays. Values which were int in _defaults must remain int and an error should be raised if a float value is proposed. 8) keys listed in _parametersWithUnits can be assigned with tuples (value, "unit"). They will be converted automatically with check_units(value). 9) for parameters with a default value None, any value is acceptable 10) A clear error message should be displayed for any bad value showing the current value of the parameter and its default value.
Expand source code
def update(self, **kwargs): """ Update layer parameters following strict validation rules. Rules: 1) key should be listed in self._defaults 2) for some keys, synonyms are acceptable as reported in self._synonyms 3) values cannot be None if they were not None in _defaults 4) values should be str if they were initially str, idem with bool 5) values which were numeric (int, float, np.ndarray) should remain numeric. 6) lists are acceptable as numeric arrays 7) all numerical (float, np.ndarray, list) except int must be converted into numpy arrays. Values which were int in _defaults must remain int and an error should be raised if a float value is proposed. 8) keys listed in _parametersWithUnits can be assigned with tuples (value, "unit"). They will be converted automatically with check_units(value). 9) for parameters with a default value None, any value is acceptable 10) A clear error message should be displayed for any bad value showing the current value of the parameter and its default value. """ if not kwargs: # shortcut return self # for chaining param_counts = {key: 0 for key in self._defaults} # Track how many times each param is set def resolve_key(key): """Resolve key considering synonyms and check for duplicates.""" for main_key, synonyms in self._synonyms.items(): if key == main_key or key in synonyms: param_counts[main_key] += 1 return main_key param_counts[key] += 1 return key def validate_value(key, value): """Validate and process the value according to the rules.""" default_value = self._defaults[key] # Rule 3: values cannot be None if they were not None in _defaults if value is None and default_value is not None: raise ValueError(f"Invalid value for '{key}': None is not allowed. " f"Current: {getattr(self, key)}, Default: {default_value}") # Rule 9: If default is None, any value is acceptable if default_value is None: return value # Rule 4 & 5: Ensure type consistency (str, bool, or numeric types) if isinstance(default_value, str) and not isinstance(value, str): raise TypeError(f"Invalid type for '{key}': Expected str, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") if isinstance(default_value, bool) and not isinstance(value, bool): raise TypeError(f"Invalid type for '{key}': Expected bool, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") # Rule 6 & 7: Convert numeric types properly if isinstance(default_value, (int, float, np.ndarray)): if isinstance(value, list): value = np.array(value) if isinstance(default_value, int): if isinstance(value, float) or (isinstance(value, np.ndarray) and np.issubdtype(value.dtype, np.floating)): raise TypeError(f"Invalid type for '{key}': Expected integer, got float. " f"Current: {getattr(self, key)}, Default: {default_value}") if isinstance(value, (int, np.integer)): return int(value) # Ensure it remains an int raise TypeError(f"Invalid type for '{key}': Expected integer, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") if isinstance(value, (int, float, list, np.ndarray)): return np.array(value, dtype=float) # Convert everything to np.array for floats raise TypeError(f"Invalid type for '{key}': Expected numeric, got {type(value).__name__}. " f"Current: {getattr(self, key)}, Default: {default_value}") # Rule 8: Convert units if applicable if key in self._parametersWithUnits and isinstance(value, tuple): value, unit = value converted_value, _ = check_units((value, unit), ExpectedUnits=self._parametersWithUnits[key]) return converted_value return value # Apply updates while tracking parameter occurrences for key, value in kwargs.items(): resolved_key = resolve_key(key) if resolved_key not in self._defaults: raise KeyError(f"Invalid key '{key}'. Allowed keys: {list(self._defaults.keys())}.") try: validated_value = validate_value(resolved_key, value) setattr(self, resolved_key, validated_value) except (TypeError, ValueError) as e: raise ValueError(f"Error updating '{key}': {e}") # Ensure that no parameter was set multiple times due to synonyms duplicate_keys = [k for k, v in param_counts.items() if v > 1] if duplicate_keys: raise ValueError(f"Duplicate assignment detected for parameters: {duplicate_keys}. " "Use only one synonym per parameter.") return self # to enable chaining
class layerLink (property='D', indices=None, values=None, length=None, replacement='repeat', dtype=numpy.float64, maxlength=None)
-
A sparse representation of properties (
D
,k
,C0
) used inlayer
instances.This class allows storing and manipulating selected values of a property (<code>D</code>, <code>k</code>, or <code>C0</code>) while keeping a sparse structure. It enables seamless interaction with <code><a title="layer.layer" href="#layer.layer">layer</a></code> objects by overriding values dynamically and ensuring efficient memory usage. The primary use case is to fit and control property values externally while keeping the <code><a title="layer.layer" href="#layer.layer">layer</a></code> representation internally consistent. Attributes ---------- property : str The name of the property linked (`"D"`, `"k"`, or `"C0"`). indices : np.ndarray A NumPy array storing the indices of explicitly defined values. values : np.ndarray A NumPy array storing the corresponding values at <code>indices</code>. length : int The total length of the sparse vector, ensuring coverage of all indices. replacement : str, optional Defines how missing values are handled: - `"repeat"`: Propagates the last known value beyond <code>length</code>. - `"periodic"`: Cycles through known values beyond <code>length</code>. - Default: No automatic replacement within <code>length</code>. Methods ------- set(index, value) Sets values at specific indices. If <code>None</code> or <code>np.nan</code> is provided, the index is removed. get(index=None) Retrieves values at the given indices. Returns <code>NaN</code> for missing values. getandreplace(indices, altvalues) Similar to <code>get()</code>, but replaces <code>NaN</code> values with corresponding values from <code>altvalues</code>. getfull(altvalues) Returns the full vector using <code>getandreplace(None, altvalues)</code>. lengthextension() Ensures <code>length</code> covers all stored indices (`max(indices) + 1`). rename(new_property_name) Renames the <code>property</code> associated with this <code><a title="layer.layerLink" href="#layer.layerLink">layerLink</a></code>. nzcount() Returns the number of explicitly stored (nonzero) elements. __getitem__(index) Allows retrieval using <code>D\_link\[index]</code>, equivalent to <code>get(index)</code>. __setitem__(index, value) Allows assignment using `D_link[index] = value`, equivalent to <code>set(index, value)</code>. __add__(other) Concatenates two <code><a title="layer.layerLink" href="#layer.layerLink">layerLink</a></code> instances with the same property. __mul__(n) Repeats the <code><a title="layer.layerLink" href="#layer.layerLink">layerLink</a></code> instance <code>n</code> times, shifting indices accordingly. Examples -------- Create a <code><a title="layer.layerLink" href="#layer.layerLink">layerLink</a></code> for <code>D</code> and manipulate its values: ```python D_link = layerLink("D") D_link.set([0, 2], [1e-14, 3e-14]) print(D_link.get()) # Expected: array([1e-14, nan, 3e-14]) D_link[1] = 2e-14 print(D_link.get()) # Expected: array([1e-14, 2e-14, 3e-14]) ``` Concatenating two <code><a title="layer.layerLink" href="#layer.layerLink">layerLink</a></code> instances: ```python A = layerLink("D") A.set([0, 2], [1e-14, 3e-14]) B = layerLink("D") B.set([1, 3], [2e-14, 4e-14]) C = A + B # Concatenates while shifting indices print(C.get()) # Expected: array([1e-14, 3e-14, nan, nan, 2e-14, 4e-14]) ``` Handling missing values with <code>getandreplace()</code>: ```python alt_values = np.array([5e-14, 6e-14, 7e-14, 8e-14]) print(D_link.getandreplace([0, 1, 2, 3], alt_values)) # Expected: array([1e-14, 2e-14, 3e-14, 8e-14]) # Fills NaNs from alt_values ``` Ensuring correct behavior for `*`: ```python B = A * 3 # Repeats A three times print(B.indices) # Expected: [0, 2, 4, 6, 8, 10] print(B.values) # Expected: [1e-14, 3e-14, 1e-14, 3e-14, 1e-14, 3e-14] print(B.length) # Expected: 3 * A.length ``` Other Examples: ---------------- ### **Creating a Link** D_link = layerLink("D", indices=[1, 3], values=[5e-14, 7e-14], length=4) print(D_link) # <Link for D: 2 of 4 replacement values> ### **Retrieving Values** print(D_link.get()) # Full vector with None in unspecified indices print(D_link.get(1)) # Returns 5e-14 print(D_link.get([0,2])) # Returns [None, None] ### **Setting Values** D_link.set(2, 6e-14) print(D_link.get()) # Now index 2 is replaced ### **Resetting with a Prototype** prototype = [None, 5e-14, None, 7e-14, 8e-14] D_link.reset(prototype) print(D_link.get()) # Now follows the new structure ### **Getting and Setting Values with []** D_link = layerLink("D", indices=[1, 3, 5], values=[5e-14, 7e-14, 6e-14], length=10) print(D_link[3]) # ✅ Returns 7e-14 print(D_link[:5]) # ✅ Returns first 5 elements (with NaNs where undefined) print(D_link[[1, 3]]) # ✅ Returns [5e-14, 7e-14] D_link[2] = 9e-14 # ✅ Sets D[2] to 9e-14 D_link[0:4:2] = [1e-14, 2e-14] # ✅ Sets D[0] = 1e-14, D[2] = 2e-14 print(len(D_link)) # ✅ Returns 10 (full vector length) ###**Practical Syntaxes** D_link = layerLink("D") D_link[2] = 3e-14 # ✅ single value D_link[0] = 1e-14 print(D_link.get()) print(D_link[1]) print(repr(D_link)) D_link[:4] = 1e-16 # ✅ Fills indices 0,1,2,3 with 1e-16 print(D_link.get()) # ✅ Outputs: [1e-16, 1e-16, 1e-16, 1e-16, nan, 1e-14] D_link[[1,2]] = None # ✅ Fills indices 0,1,2,3 with 1e-16 print(D_link.get()) # ✅ Outputs: [1e-16, 1e-16, 1e-16, 1e-16, nan, 1e-14] D_link[[0]] = 1e-10 print(D_link.get()) ###**How it works inside layer: a short simulation** # layerLink created by user duser = layerLink() duser.getfull([1e-15,2e-15,3e-15]) duser[0] = 1e-10 duser.getfull([1e-15,2e-15,3e-15]) duser[1]=1e-9 duser.getfull([1e-15,2e-15,3e-15]) # layerLink used internally dalias=duser dalias[1]=2e-11 duser.getfull([1e-15,2e-15,3e-15,4e-15]) dalias[1]=2.1e-11 duser.getfull([1e-15,2e-15,3e-15,4e-15]) ###**Combining layerLinks instances** A = layerLink("D") A.set([0, 2], [1e-11, 3e-11]) # length=3 B = layerLink("D") B.set([1, 3], [2e-14, 4e-12]) # length=4 C = A + B print(C.indices) # Expected: [0, 2, 4, 6] print(C.values) # Expected: [1.e-11 3.e-11 2.e-14 4.e-12] print(C.length) # Expected: 3 + 4 = 7 TEST CASES: ----------- print("🔹 Test 1: Initialize empty layerLink") D_link = layerLink("D") print(D_link.get()) # Expected: array([]) or array([nan, nan, nan]) if length is pre-set print(repr(D_link)) # Expected: No indices set print("
🔹 Test 2: Assigning values at specific indices") D_link[2] = 3e-14 D_link[0] = 1e-14 print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14]) print(D_link[1]) # Expected: nan
print("
🔹 Test 3: Assign multiple values at once") D_link[[1, 4]] = [2e-14, 5e-14] print(D_link.get()) # Expected: array([1.e-14, 2.e-14, 3.e-14, nan, 5.e-14])
print("
🔹 Test 4: Remove a single index") D_link[1] = None print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14, nan, 5.e-14])
print("
🔹 Test 5: Remove multiple indices at once") D_link[[0, 2]] = None print(D_link.get()) # Expected: array([nan, nan, nan, nan, 5.e-14])
print("
🔹 Test 6: Removing indices using a slice") D_link[3:5] = None print(D_link.get()) # Expected: array([nan, nan, nan, nan, nan])
print("
🔹 Test 7: Assign new values after removals") D_link[1] = 7e-14 D_link[3] = 8e-14 print(D_link.get()) # Expected: array([nan, 7.e-14, nan, 8.e-14, nan])
print("
🔹 Test 8: Check periodic replacement") D_link = layerLink("D", replacement="periodic") D_link[2] = 3e-14 D_link[0] = 1e-14 print(D_link[5]) # Expected: 1e-14 (since 5 mod 2 = 0)
print("
🔹 Test 9: Check repeat replacement") D_link = layerLink("D", replacement="repeat") D_link[2] = 3e-14 D_link[0] = 1e-14 print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14]) print(D_link[3]) # Expected: 3e-14 (repeat last known value)
print("
🔹 Test 10: Resetting with a prototype") D_link.reset([None, 5e-14, None, 7e-14]) print(D_link.get()) # Expected: array([nan, 5.e-14, nan, 7.e-14])
print("
🔹 Test 11: Edge case - Assigning nan explicitly") D_link[1] = np.nan print(D_link.get()) # Expected: array([nan, nan, nan, 7.e-14])
print("
🔹 Test 12: Assigning a range with a scalar value (broadcasting)") D_link[0:3] = 9e-14 print(D_link.get()) # Expected: array([9.e-14, 9.e-14, 9.e-14, 7.e-14])
print("
🔹 Test 13: Assigning a slice with a list of values") D_link[1:4] = [6e-14, 5e-14, 4e-14] print(D_link.get()) # Expected: array([9.e-14, 6.e-14, 5.e-14, 4.e-14])
print("
🔹 Test 14: Length updates correctly after removals") D_link[[1, 2]] = None print(len(D_link)) # Expected: 4 (since max index is 3)
print("
🔹 Test 15: Setting index beyond length auto-extends") D_link[6] = 2e-14 print(len(D_link)) # Expected: 7 (since max index is 6) print(D_link.get()) # Expected: array([9.e-14, nan, nan, 4.e-14, nan, nan, 2.e-14])
constructs a link
Expand source code
class layerLink: """ A sparse representation of properties (`D`, `k`, `C0`) used in `layer` instances. This class allows storing and manipulating selected values of a property (`D`, `k`, or `C0`) while keeping a sparse structure. It enables seamless interaction with `layer` objects by overriding values dynamically and ensuring efficient memory usage. The primary use case is to fit and control property values externally while keeping the `layer` representation internally consistent. Attributes ---------- property : str The name of the property linked (`"D"`, `"k"`, or `"C0"`). indices : np.ndarray A NumPy array storing the indices of explicitly defined values. values : np.ndarray A NumPy array storing the corresponding values at `indices`. length : int The total length of the sparse vector, ensuring coverage of all indices. replacement : str, optional Defines how missing values are handled: - `"repeat"`: Propagates the last known value beyond `length`. - `"periodic"`: Cycles through known values beyond `length`. - Default: No automatic replacement within `length`. Methods ------- set(index, value) Sets values at specific indices. If `None` or `np.nan` is provided, the index is removed. get(index=None) Retrieves values at the given indices. Returns `NaN` for missing values. getandreplace(indices, altvalues) Similar to `get()`, but replaces `NaN` values with corresponding values from `altvalues`. getfull(altvalues) Returns the full vector using `getandreplace(None, altvalues)`. lengthextension() Ensures `length` covers all stored indices (`max(indices) + 1`). rename(new_property_name) Renames the `property` associated with this `layerLink`. nzcount() Returns the number of explicitly stored (nonzero) elements. __getitem__(index) Allows retrieval using `D_link[index]`, equivalent to `get(index)`. __setitem__(index, value) Allows assignment using `D_link[index] = value`, equivalent to `set(index, value)`. __add__(other) Concatenates two `layerLink` instances with the same property. __mul__(n) Repeats the `layerLink` instance `n` times, shifting indices accordingly. Examples -------- Create a `layerLink` for `D` and manipulate its values: ```python D_link = layerLink("D") D_link.set([0, 2], [1e-14, 3e-14]) print(D_link.get()) # Expected: array([1e-14, nan, 3e-14]) D_link[1] = 2e-14 print(D_link.get()) # Expected: array([1e-14, 2e-14, 3e-14]) ``` Concatenating two `layerLink` instances: ```python A = layerLink("D") A.set([0, 2], [1e-14, 3e-14]) B = layerLink("D") B.set([1, 3], [2e-14, 4e-14]) C = A + B # Concatenates while shifting indices print(C.get()) # Expected: array([1e-14, 3e-14, nan, nan, 2e-14, 4e-14]) ``` Handling missing values with `getandreplace()`: ```python alt_values = np.array([5e-14, 6e-14, 7e-14, 8e-14]) print(D_link.getandreplace([0, 1, 2, 3], alt_values)) # Expected: array([1e-14, 2e-14, 3e-14, 8e-14]) # Fills NaNs from alt_values ``` Ensuring correct behavior for `*`: ```python B = A * 3 # Repeats A three times print(B.indices) # Expected: [0, 2, 4, 6, 8, 10] print(B.values) # Expected: [1e-14, 3e-14, 1e-14, 3e-14, 1e-14, 3e-14] print(B.length) # Expected: 3 * A.length ``` Other Examples: ---------------- ### **Creating a Link** D_link = layerLink("D", indices=[1, 3], values=[5e-14, 7e-14], length=4) print(D_link) # <Link for D: 2 of 4 replacement values> ### **Retrieving Values** print(D_link.get()) # Full vector with None in unspecified indices print(D_link.get(1)) # Returns 5e-14 print(D_link.get([0,2])) # Returns [None, None] ### **Setting Values** D_link.set(2, 6e-14) print(D_link.get()) # Now index 2 is replaced ### **Resetting with a Prototype** prototype = [None, 5e-14, None, 7e-14, 8e-14] D_link.reset(prototype) print(D_link.get()) # Now follows the new structure ### **Getting and Setting Values with []** D_link = layerLink("D", indices=[1, 3, 5], values=[5e-14, 7e-14, 6e-14], length=10) print(D_link[3]) # ✅ Returns 7e-14 print(D_link[:5]) # ✅ Returns first 5 elements (with NaNs where undefined) print(D_link[[1, 3]]) # ✅ Returns [5e-14, 7e-14] D_link[2] = 9e-14 # ✅ Sets D[2] to 9e-14 D_link[0:4:2] = [1e-14, 2e-14] # ✅ Sets D[0] = 1e-14, D[2] = 2e-14 print(len(D_link)) # ✅ Returns 10 (full vector length) ###**Practical Syntaxes** D_link = layerLink("D") D_link[2] = 3e-14 # ✅ single value D_link[0] = 1e-14 print(D_link.get()) print(D_link[1]) print(repr(D_link)) D_link[:4] = 1e-16 # ✅ Fills indices 0,1,2,3 with 1e-16 print(D_link.get()) # ✅ Outputs: [1e-16, 1e-16, 1e-16, 1e-16, nan, 1e-14] D_link[[1,2]] = None # ✅ Fills indices 0,1,2,3 with 1e-16 print(D_link.get()) # ✅ Outputs: [1e-16, 1e-16, 1e-16, 1e-16, nan, 1e-14] D_link[[0]] = 1e-10 print(D_link.get()) ###**How it works inside layer: a short simulation** # layerLink created by user duser = layerLink() duser.getfull([1e-15,2e-15,3e-15]) duser[0] = 1e-10 duser.getfull([1e-15,2e-15,3e-15]) duser[1]=1e-9 duser.getfull([1e-15,2e-15,3e-15]) # layerLink used internally dalias=duser dalias[1]=2e-11 duser.getfull([1e-15,2e-15,3e-15,4e-15]) dalias[1]=2.1e-11 duser.getfull([1e-15,2e-15,3e-15,4e-15]) ###**Combining layerLinks instances** A = layerLink("D") A.set([0, 2], [1e-11, 3e-11]) # length=3 B = layerLink("D") B.set([1, 3], [2e-14, 4e-12]) # length=4 C = A + B print(C.indices) # Expected: [0, 2, 4, 6] print(C.values) # Expected: [1.e-11 3.e-11 2.e-14 4.e-12] print(C.length) # Expected: 3 + 4 = 7 TEST CASES: ----------- print("🔹 Test 1: Initialize empty layerLink") D_link = layerLink("D") print(D_link.get()) # Expected: array([]) or array([nan, nan, nan]) if length is pre-set print(repr(D_link)) # Expected: No indices set print("\n🔹 Test 2: Assigning values at specific indices") D_link[2] = 3e-14 D_link[0] = 1e-14 print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14]) print(D_link[1]) # Expected: nan print("\n🔹 Test 3: Assign multiple values at once") D_link[[1, 4]] = [2e-14, 5e-14] print(D_link.get()) # Expected: array([1.e-14, 2.e-14, 3.e-14, nan, 5.e-14]) print("\n🔹 Test 4: Remove a single index") D_link[1] = None print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14, nan, 5.e-14]) print("\n🔹 Test 5: Remove multiple indices at once") D_link[[0, 2]] = None print(D_link.get()) # Expected: array([nan, nan, nan, nan, 5.e-14]) print("\n🔹 Test 6: Removing indices using a slice") D_link[3:5] = None print(D_link.get()) # Expected: array([nan, nan, nan, nan, nan]) print("\n🔹 Test 7: Assign new values after removals") D_link[1] = 7e-14 D_link[3] = 8e-14 print(D_link.get()) # Expected: array([nan, 7.e-14, nan, 8.e-14, nan]) print("\n🔹 Test 8: Check periodic replacement") D_link = layerLink("D", replacement="periodic") D_link[2] = 3e-14 D_link[0] = 1e-14 print(D_link[5]) # Expected: 1e-14 (since 5 mod 2 = 0) print("\n🔹 Test 9: Check repeat replacement") D_link = layerLink("D", replacement="repeat") D_link[2] = 3e-14 D_link[0] = 1e-14 print(D_link.get()) # Expected: array([1.e-14, nan, 3.e-14]) print(D_link[3]) # Expected: 3e-14 (repeat last known value) print("\n🔹 Test 10: Resetting with a prototype") D_link.reset([None, 5e-14, None, 7e-14]) print(D_link.get()) # Expected: array([nan, 5.e-14, nan, 7.e-14]) print("\n🔹 Test 11: Edge case - Assigning nan explicitly") D_link[1] = np.nan print(D_link.get()) # Expected: array([nan, nan, nan, 7.e-14]) print("\n🔹 Test 12: Assigning a range with a scalar value (broadcasting)") D_link[0:3] = 9e-14 print(D_link.get()) # Expected: array([9.e-14, 9.e-14, 9.e-14, 7.e-14]) print("\n🔹 Test 13: Assigning a slice with a list of values") D_link[1:4] = [6e-14, 5e-14, 4e-14] print(D_link.get()) # Expected: array([9.e-14, 6.e-14, 5.e-14, 4.e-14]) print("\n🔹 Test 14: Length updates correctly after removals") D_link[[1, 2]] = None print(len(D_link)) # Expected: 4 (since max index is 3) print("\n🔹 Test 15: Setting index beyond length auto-extends") D_link[6] = 2e-14 print(len(D_link)) # Expected: 7 (since max index is 6) print(D_link.get()) # Expected: array([9.e-14, nan, nan, 4.e-14, nan, nan, 2.e-14]) """ def __init__(self, property="D", indices=None, values=None, length=None, replacement="repeat", dtype=np.float64, maxlength=None): """constructs a link""" self.property = property # "D", "k", or "C0" self.replacement = replacement self.dtype = dtype self._maxlength = maxlength if isinstance(indices,(int,float)): indices = [indices] if isinstance(values,(int,float)): values = [values] if indices is None or values is None: self.indices = np.array([], dtype=int) self.values = np.array([], dtype=dtype) else: self.indices = np.array(indices, dtype=int) self.values = np.array(values, dtype=dtype) self.length = length if length is not None else (self.indices.max() + 1 if self.indices.size > 0 else 0) self._validate() def _validate(self): """Ensures consistency between indices and values.""" if len(self.indices) != len(self.values): raise ValueError("indices and values must have the same length.") if self.indices.size > 0 and self.length < self.indices.max() + 1: raise ValueError("length must be at least max(indices) + 1.") def reset(self, prototypevalues): """ Resets the link instance based on the prototype values. - Stores only non-None values. - Updates `indices`, `values`, and `length` accordingly. """ self.indices = np.array([i for i, v in enumerate(prototypevalues) if v is not None], dtype=int) self.values = np.array([v for v in prototypevalues if v is not None], dtype=self.dtype) self.length = len(prototypevalues) # Update the total length def get(self, index=None): """ Retrieves values based on index or returns the full vector. Rules: - If `index=None`, returns the full vector with overridden values (no replacement applied). - If `index` is a scalar, returns the corresponding value, applying replacement rules if needed. - If `index` is an array, returns an array of the requested indices, applying replacement rules. Returns: - NumPy array with requested values. """ if index is None: # Return the full vector WITHOUT applying any replacement full_vector = np.full(self.length, np.nan, dtype=self.dtype) full_vector[self.indices] = self.values # Set known values return full_vector if np.isscalar(index): return self._get_single(index) # Ensure index is an array index = np.array(index, dtype=int) return np.array([self._get_single(i) for i in index], dtype=self.dtype) def _get_single(self, i): """Retrieves the value for a single index, applying rules if necessary.""" if i in self.indices: return self.values[np.where(self.indices == i)[0][0]] if i >= self.length: # Apply replacement *only* for indices beyond length if self.replacement == "periodic": return self.values[i % len(self.values)] elif self.replacement == "repeat": return self._get_single(self.length - 1) # Repeat last known value return np.nan # Default case for undefined in-bounds indices def set(self, index, value): """ Sets values at specific indices. - If `index=None`, resets the link with `value`. - If `index` is a scalar, updates or inserts the value. - If `index` is an array, updates corresponding values. - If `value` is `None` or `np.nan`, removes the corresponding index. """ if index is None: self.reset(value) return index = np.array(index, dtype=int) value = np.array(value, dtype=self.dtype) # check against _maxlength if defined if self._maxlength is not None: if np.any(index>=self._maxlength): raise IndexError(f"index cannot exceeds the number of layers-1 {self._maxlength-1}") # Handle scalars properly if np.isscalar(index): index = np.array([index]) value = np.array([value]) # Detect None or NaN values and remove those indices mask = np.isnan(value) if value.dtype.kind == 'f' else np.array([v is None for v in value]) if np.any(mask): self._remove_indices(index[mask]) # Remove these indices index, value = index[~mask], value[~mask] # Keep only valid values if index.size > 0: # If there are remaining valid values, store them for i, v in zip(index, value): if i in self.indices: self.values[np.where(self.indices == i)[0][0]] = v else: self.indices = np.append(self.indices, i) self.values = np.append(self.values, v) # Update length to ensure it remains valid if self.indices.size > 0: self.length = max(self.indices) + 1 # Adjust length based on max index else: self.length = 0 # Reset to 0 if empty self._validate() def _remove_indices(self, indices): """ Removes indices from `self.indices` and `self.values` and updates length. """ mask = np.isin(self.indices, indices, invert=True) self.indices = self.indices[mask] self.values = self.values[mask] # Update length after removal if self.indices.size > 0: self.length = max(self.indices) + 1 # Adjust length based on remaining max index else: self.length = 0 # Reset to 0 if no indices remain def reshape(self, new_length): """ Reshapes the link instance to a new length. - If indices exceed new_length-1, they are removed with a warning. - If replacement operates beyond new_length-1, a warning is issued. """ if new_length < self.length: invalid_indices = self.indices[self.indices >= new_length] if invalid_indices.size > 0: print(f"⚠️ Warning: Indices {invalid_indices.tolist()} are outside new length {new_length}. They will be removed.") mask = self.indices < new_length self.indices = self.indices[mask] self.values = self.values[mask] # Check if replacement would be applied beyond the new length if self.replacement == "repeat" and self.indices.size > 0 and self.length > new_length: print(f"⚠️ Warning: Repeat rule was defined for indices beyond {new_length-1}, but will not be used.") if self.replacement == "periodic" and self.indices.size > 0 and self.length > new_length: print(f"⚠️ Warning: Periodic rule was defined for indices beyond {new_length-1}, but will not be used.") self.length = new_length def __repr__(self): """Returns a detailed string representation.""" txt = (f"Link(property='{self.property}', indices={self.indices.tolist()}, " f"values={self.values.tolist()}, length={self.length}, replacement='{self.replacement}')") print(txt) return(str(self)) def __str__(self): """Returns a compact summary string.""" return f"<{self.property}:{self.__class__.__name__}: {len(self.indices)}/{self.length} values>" # Override `len()` def __len__(self): """Returns the length of the vector managed by the link object.""" return self.length # Override `getitem` (support for indexing and slicing) def __getitem__(self, index): """ Allows `D_link[index]` or `D_link[slice]` to retrieve values. - If `index` is an integer, returns a single value. - If `index` is a slice or list/array, returns a NumPy array of values. """ if isinstance(index, slice): return self.get(np.arange(index.start or 0, index.stop or self.length, index.step or 1)) return self.get(index) # Override `setitem` (support for indexing and slicing) def __setitem__(self, index, value): """ Allows `D_link[index] = value` or `D_link[slice] = list/scalar`. - If `index` is an integer, updates or inserts a single value. - If `index` is a slice or list/array, updates multiple values. - If `value` is `None` or `np.nan`, removes the corresponding index. """ if isinstance(index, slice): indices = np.arange(index.start or 0, index.stop or self.length, index.step or 1) elif isinstance(index, (list, np.ndarray)): # Handle non-contiguous indices indices = np.array(index, dtype=int) elif np.isscalar(index): # Single index assignment indices = np.array([index], dtype=int) else: raise TypeError(f"Unsupported index type: {type(index)}") if value is None or (isinstance(value, float) and np.isnan(value)): # Remove these indices self._remove_indices(indices) else: values = np.full_like(indices, value, dtype=self.dtype) if np.isscalar(value) else np.array(value, dtype=self.dtype) if len(indices) != len(values): raise ValueError(f"Cannot assign {len(values)} values to {len(indices)} indices.") self.set(indices, values) def getandreplace(self, indices=None, altvalues=None): """ Retrieves values for the given indices, replacing NaN values with corresponding values from altvalues. - If `indices` is None or empty, it defaults to `[0, 1, ..., self.length - 1]` - altvalues should be a NumPy array with the same dtype as self.values. - altvalues **can be longer than** self.length, but **cannot be shorter than the highest requested index**. - If an index is undefined (`NaN` in get()), it is replaced with altvalues[index]. Parameters: ---------- indices : list or np.ndarray (default: None) The indices to retrieve values for. If None, defaults to full range `[0, ..., self.length - 1]`. altvalues : list or np.ndarray Alternative values to use where `get()` returns `NaN`. Returns: ------- np.ndarray A NumPy array of values, with NaNs replaced by altvalues. """ if indices is None or len(indices) == 0: indices = np.arange(self.length) # Default to full range indices = np.array(indices, dtype=int) altvalues = np.array(altvalues, dtype=self.dtype) max_requested_index = indices.max() if indices.size > 0 else 0 if max_requested_index >= altvalues.shape[0]: # Ensure altvalues covers all requested indices raise ValueError( f"altvalues is too short! It has length {altvalues.shape[0]}, but requested index {max_requested_index}." ) # Get original values original_values = self.get(indices) # Replace NaN values with corresponding values from altvalues mask_nan = np.isnan(original_values) original_values[mask_nan] = altvalues[indices[mask_nan]] return original_values def getfull(self, altvalues): """ Retrieves the full vector using `getandreplace(None, altvalues)`. - If `length == 0`, returns `altvalues` as a NumPy array of the correct dtype. - Extends `self.length` to match `altvalues` if it's shorter. - Supports multidimensional `altvalues` by flattening it. Parameters: ---------- altvalues : list or np.ndarray Alternative values to use where `get()` returns `NaN`. Returns: ------- np.ndarray Full vector with NaNs replaced by altvalues. """ # Convert altvalues to a NumPy array and flatten if needed altvalues = np.array(altvalues, dtype=self.dtype).flatten() # If self has no length, return altvalues directly if self.length == 0: return altvalues # Extend self.length to match altvalues if needed if self.length < altvalues.shape[0]: self.length = altvalues.shape[0] return self.getandreplace(None, altvalues) @property def nzlength(self): """ Returns the number of stored nonzero elements (i.e., indices with values). """ return len(self.indices) def lengthextension(self): """ Ensures that the length of the layerLink instance is at least `max(indices) + 1`. - If there are no indices, the length remains unchanged. - If `length` is already sufficient, nothing happens. - Otherwise, it extends `length` to `max(indices) + 1`. """ if self.indices.size > 0: # Only extend if there are indices self.length = max(self.length, max(self.indices) + 1) def rename(self, new_property_name): """ Renames the property associated with this link. Parameters: ---------- new_property_name : str The new property name. Raises: ------- TypeError: If `new_property_name` is not a string. """ if not isinstance(new_property_name, str): raise TypeError(f"Property name must be a string, got {type(new_property_name).__name__}.") self.property = new_property_name def __add__(self, other): """ Concatenates two layerLink instances. - Only allowed if both instances have the same property. - Calls `lengthextension()` on both instances before summing lengths. - Shifts `other`'s indices by `self.length` to maintain sparsity. - Concatenates values and indices. Returns: ------- layerLink A new concatenated layerLink instance. """ if not isinstance(other, layerLink): raise TypeError(f"Cannot concatenate {type(self).__name__} with {type(other).__name__}") if self.property != other.property: raise ValueError(f"Cannot concatenate: properties do not match ('{self.property}' vs. '{other.property}')") # Ensure lengths are properly extended before computing new length self.lengthextension() other.lengthextension() # Create a new instance for the result result = layerLink(self.property) # Copy self's values result.indices = np.array(self.indices, dtype=int) result.values = np.array(self.values, dtype=self.dtype) # Adjust other’s indices and add them shifted_other_indices = np.array(other.indices) + self.length result.indices = np.concatenate([result.indices, shifted_other_indices]) result.values = np.concatenate([result.values, np.array(other.values, dtype=self.dtype)]) # ✅ Correct length calculation: Sum of the two lengths (assuming lengths are extended) result.length = self.length + other.length return result def __mul__(self, n): """ Repeats the layerLink instance `n` times. - Uses `+` to concatenate multiple copies with shifted indices. - Each repetition gets indices shifted by `self.length * i`. Returns: ------- layerLink A new layerLink instance with repeated data. """ if not isinstance(n, int) or n <= 0: raise ValueError("Multiplication factor must be a positive integer") result = layerLink(self.property) for i in range(n): shifted_instance = layerLink(self.property) shifted_instance.indices = np.array(self.indices) + i * self.length shifted_instance.values = np.array(self.values, dtype=self.dtype) shifted_instance.length = self.length result += shifted_instance # Use `+` to merge each repetition return result
Instance variables
var nzlength
-
Returns the number of stored nonzero elements (i.e., indices with values).
Expand source code
@property def nzlength(self): """ Returns the number of stored nonzero elements (i.e., indices with values). """ return len(self.indices)
Methods
def get(self, index=None)
-
Retrieves values based on index or returns the full vector.
Rules: - If
index=None
, returns the full vector with overridden values (no replacement applied). - Ifindex
is a scalar, returns the corresponding value, applying replacement rules if needed. - Ifindex
is an array, returns an array of the requested indices, applying replacement rules.Returns: - NumPy array with requested values.
Expand source code
def get(self, index=None): """ Retrieves values based on index or returns the full vector. Rules: - If `index=None`, returns the full vector with overridden values (no replacement applied). - If `index` is a scalar, returns the corresponding value, applying replacement rules if needed. - If `index` is an array, returns an array of the requested indices, applying replacement rules. Returns: - NumPy array with requested values. """ if index is None: # Return the full vector WITHOUT applying any replacement full_vector = np.full(self.length, np.nan, dtype=self.dtype) full_vector[self.indices] = self.values # Set known values return full_vector if np.isscalar(index): return self._get_single(index) # Ensure index is an array index = np.array(index, dtype=int) return np.array([self._get_single(i) for i in index], dtype=self.dtype)
def getandreplace(self, indices=None, altvalues=None)
-
Retrieves values for the given indices, replacing NaN values with corresponding values from altvalues.
- If
indices
is None or empty, it defaults to[0, 1, ..., self.length - 1]
- altvalues should be a NumPy array with the same dtype as self.values.
- altvalues can be longer than self.length, but cannot be shorter than the highest requested index.
- If an index is undefined (
NaN
in get()), it is replaced with altvalues[index].
Parameters:
indices : list or np.ndarray (default: None) The indices to retrieve values for. If None, defaults to full range
[0, ..., self.length - 1]
. altvalues : list or np.ndarray Alternative values to use whereget()
returnsNaN
.Returns:
np.ndarray A NumPy array of values, with NaNs replaced by altvalues.
Expand source code
def getandreplace(self, indices=None, altvalues=None): """ Retrieves values for the given indices, replacing NaN values with corresponding values from altvalues. - If `indices` is None or empty, it defaults to `[0, 1, ..., self.length - 1]` - altvalues should be a NumPy array with the same dtype as self.values. - altvalues **can be longer than** self.length, but **cannot be shorter than the highest requested index**. - If an index is undefined (`NaN` in get()), it is replaced with altvalues[index]. Parameters: ---------- indices : list or np.ndarray (default: None) The indices to retrieve values for. If None, defaults to full range `[0, ..., self.length - 1]`. altvalues : list or np.ndarray Alternative values to use where `get()` returns `NaN`. Returns: ------- np.ndarray A NumPy array of values, with NaNs replaced by altvalues. """ if indices is None or len(indices) == 0: indices = np.arange(self.length) # Default to full range indices = np.array(indices, dtype=int) altvalues = np.array(altvalues, dtype=self.dtype) max_requested_index = indices.max() if indices.size > 0 else 0 if max_requested_index >= altvalues.shape[0]: # Ensure altvalues covers all requested indices raise ValueError( f"altvalues is too short! It has length {altvalues.shape[0]}, but requested index {max_requested_index}." ) # Get original values original_values = self.get(indices) # Replace NaN values with corresponding values from altvalues mask_nan = np.isnan(original_values) original_values[mask_nan] = altvalues[indices[mask_nan]] return original_values
- If
def getfull(self, altvalues)
-
Retrieves the full vector using
getandreplace(None, altvalues)
.- If
length == 0
, returnsaltvalues
as a NumPy array of the correct dtype. - Extends
self.length
to matchaltvalues
if it's shorter. - Supports multidimensional
altvalues
by flattening it.
Parameters:
altvalues : list or np.ndarray Alternative values to use where
get()
returnsNaN
.Returns:
np.ndarray Full vector with NaNs replaced by altvalues.
Expand source code
def getfull(self, altvalues): """ Retrieves the full vector using `getandreplace(None, altvalues)`. - If `length == 0`, returns `altvalues` as a NumPy array of the correct dtype. - Extends `self.length` to match `altvalues` if it's shorter. - Supports multidimensional `altvalues` by flattening it. Parameters: ---------- altvalues : list or np.ndarray Alternative values to use where `get()` returns `NaN`. Returns: ------- np.ndarray Full vector with NaNs replaced by altvalues. """ # Convert altvalues to a NumPy array and flatten if needed altvalues = np.array(altvalues, dtype=self.dtype).flatten() # If self has no length, return altvalues directly if self.length == 0: return altvalues # Extend self.length to match altvalues if needed if self.length < altvalues.shape[0]: self.length = altvalues.shape[0] return self.getandreplace(None, altvalues)
- If
def lengthextension(self)
-
Ensures that the length of the layerLink instance is at least
max(indices) + 1
.- If there are no indices, the length remains unchanged.
- If
length
is already sufficient, nothing happens. - Otherwise, it extends
length
tomax(indices) + 1
.
Expand source code
def lengthextension(self): """ Ensures that the length of the layerLink instance is at least `max(indices) + 1`. - If there are no indices, the length remains unchanged. - If `length` is already sufficient, nothing happens. - Otherwise, it extends `length` to `max(indices) + 1`. """ if self.indices.size > 0: # Only extend if there are indices self.length = max(self.length, max(self.indices) + 1)
def rename(self, new_property_name)
-
Renames the property associated with this link.
Parameters:
new_property_name : str The new property name.
Raises:
Typeerror
If
new_property_name
is not a string.Expand source code
def rename(self, new_property_name): """ Renames the property associated with this link. Parameters: ---------- new_property_name : str The new property name. Raises: ------- TypeError: If `new_property_name` is not a string. """ if not isinstance(new_property_name, str): raise TypeError(f"Property name must be a string, got {type(new_property_name).__name__}.") self.property = new_property_name
def reset(self, prototypevalues)
-
Resets the link instance based on the prototype values.
- Stores only non-None values.
- Updates
indices
,values
, andlength
accordingly.
Expand source code
def reset(self, prototypevalues): """ Resets the link instance based on the prototype values. - Stores only non-None values. - Updates `indices`, `values`, and `length` accordingly. """ self.indices = np.array([i for i, v in enumerate(prototypevalues) if v is not None], dtype=int) self.values = np.array([v for v in prototypevalues if v is not None], dtype=self.dtype) self.length = len(prototypevalues) # Update the total length
def reshape(self, new_length)
-
Reshapes the link instance to a new length.
- If indices exceed new_length-1, they are removed with a warning.
- If replacement operates beyond new_length-1, a warning is issued.
Expand source code
def reshape(self, new_length): """ Reshapes the link instance to a new length. - If indices exceed new_length-1, they are removed with a warning. - If replacement operates beyond new_length-1, a warning is issued. """ if new_length < self.length: invalid_indices = self.indices[self.indices >= new_length] if invalid_indices.size > 0: print(f"⚠️ Warning: Indices {invalid_indices.tolist()} are outside new length {new_length}. They will be removed.") mask = self.indices < new_length self.indices = self.indices[mask] self.values = self.values[mask] # Check if replacement would be applied beyond the new length if self.replacement == "repeat" and self.indices.size > 0 and self.length > new_length: print(f"⚠️ Warning: Repeat rule was defined for indices beyond {new_length-1}, but will not be used.") if self.replacement == "periodic" and self.indices.size > 0 and self.length > new_length: print(f"⚠️ Warning: Periodic rule was defined for indices beyond {new_length-1}, but will not be used.") self.length = new_length
def set(self, index, value)
-
Sets values at specific indices.
- If
index=None
, resets the link withvalue
. - If
index
is a scalar, updates or inserts the value. - If
index
is an array, updates corresponding values. - If
value
isNone
ornp.nan
, removes the corresponding index.
Expand source code
def set(self, index, value): """ Sets values at specific indices. - If `index=None`, resets the link with `value`. - If `index` is a scalar, updates or inserts the value. - If `index` is an array, updates corresponding values. - If `value` is `None` or `np.nan`, removes the corresponding index. """ if index is None: self.reset(value) return index = np.array(index, dtype=int) value = np.array(value, dtype=self.dtype) # check against _maxlength if defined if self._maxlength is not None: if np.any(index>=self._maxlength): raise IndexError(f"index cannot exceeds the number of layers-1 {self._maxlength-1}") # Handle scalars properly if np.isscalar(index): index = np.array([index]) value = np.array([value]) # Detect None or NaN values and remove those indices mask = np.isnan(value) if value.dtype.kind == 'f' else np.array([v is None for v in value]) if np.any(mask): self._remove_indices(index[mask]) # Remove these indices index, value = index[~mask], value[~mask] # Keep only valid values if index.size > 0: # If there are remaining valid values, store them for i, v in zip(index, value): if i in self.indices: self.values[np.where(self.indices == i)[0][0]] = v else: self.indices = np.append(self.indices, i) self.values = np.append(self.values, v) # Update length to ensure it remains valid if self.indices.size > 0: self.length = max(self.indices) + 1 # Adjust length based on max index else: self.length = 0 # Reset to 0 if empty self._validate()
- If
class mesh (l, n, x0=0, index=None)
-
simple nodes class for finite-volume methods
Expand source code
class mesh(): """ simple nodes class for finite-volume methods """ def __init__(self,l,n,x0=0,index=None): self.x0 = x0 self.l = l self.n = n de = dw = l/(2*n) self.de = np.ones(n)*de self.dw = np.ones(n)*dw self.xmesh = np.linspace(0+dw,l-de,n) # nodes positions self.w = self.xmesh - dw self.e = self.xmesh + de self.index = np.full(n, int(index), dtype=np.int32) def __repr__(self): print(f"-- mesh object (layer index={self.index[0]}) --") print("%25s = %0.4g" % ("start at x0", self.x0)) print("%25s = %0.4g" % ("domain length l", self.l)) print("%25s = %0.4g" % ("number of nodes n", self.n)) print("%25s = %0.4g" % ("dw", self.dw[0])) print("%25s = %0.4g" % ("de", self.de[0])) return "mesh%d=[%0.4g %0.4g]" % \ (self.n,self.x0+self.xmesh[0],self.x0+self.xmesh[-1])
class migrant (name=None, M=None, logP=None, Dmodel='Piringer', Dtemplate={'polymer': 'LLDPE', 'M': 50, 'T': 40}, kmodel='kFHP', ktemplate={'Pi': 1.41, 'Pk': 3.97, 'Vi': 124.1, 'Vk': 30.9, 'ispolymer': True, 'alpha': 0.14, 'lngmin': 0.0, 'Psat': 1.0}, db=<patankar.loadpubchem.CompoundIndex object>, raiseerror=True)
-
A class representing a migrating chemical substance.
It can be initialized in three main ways:
1) Case (a) - By a textual name/CAS only (for a real compound search):
Example: m = migrant(name="anisole", db=my_compound_index) # or m = migrant(name="anisole", db=my_compound_index, M=None) In this mode: • A lookup is performed using db.find(name), which may return one or more records. • If multiple records match, data from each record is merged: - compound = The text used in the query (e.g. "anisole") - name = Concatenation of all distinct names from the search results - CAS = Concatenation of all CAS numbers from the search results - M = The minimum of all found molecular weights, stored in self.M (a numpy array also keeps the full set) - formula = The first formula - logP = All logP values concatenated into a numpy array (self.logP_array). The main attribute self.logP will be the same array or you may pick a single representative.
2) Case (b) - By numeric molecular weight(s) alone (generic substance):
Example: m = migrant(M=200) m = migrant(M=[100, 500]) # Possibly a range In this mode: • No search is performed. • name = "generic" (unless you override it). • compound = "single molecular weight" if 1 entry in M, or "list of molecular weights ranging from X to Y" if multiple. • CAS = None • M = the minimum of all provided M values (also stored in a numpy array) • logP = None by default, or can be supplied explicitly as an array
3) Case (c) - Name + numeric M/logP => Surrogate / hypothetical:
Example: m = migrant(name="mySurrogate", M=[200, 250], logP=[2.5, 3.0]) or m = migrant(name="surrogate", M=200) In this mode: • No lookup is performed. This is a “fake” compound not found in PubChem. • compound = "single molecular weight" or "list of molecular weights ranging from X to Y" if multiple. • name = whatever user provides • CAS = None • M = min of the provided M array, stored in a numpy array • logP = user-provided array or single float, stored in a numpy array
Attributes
compound
:str
- For case (a) => the search text; For case (b,c) => textual description of the numeric M array.
name
:str
orlist
- For case (a) => aggregated list of all found names (string-joined); For case (b) => "generic" or user-supplied name; For case (c) => user-supplied name.
CAS
:list
orNone
- For case (a) => aggregated CAS from search results; For case (b,c) => None.
M
:float
- The minimum M from either the search results or the user-supplied array.
M_array
:numpy.ndarray
- The full array of all M values found or provided.
logP
:float
ornumpy.ndarray
orNone
- For case (a) => an array of all logP from the search results (or None if not found); For case (b) => None or user-supplied value/array; For case (c) => user-supplied value/array.
Create a new migrant instance.
Parameters
name
:str
orNone
-
- A textual name for the substance to be looked up in PubChem (case a), or a custom name for a surrogate (case c).
- If None, and M is given, we treat it as a numeric-only initialization (case b).
M
:float
orlist/ndarray
offloat
orNone
-
- For case (a): If provided as None, we do a PubChem search by name.
- For case (b): The numeric molecular weight(s). No search is performed if name is None.
- For case (c): Combined name and numeric M => a surrogate with no search.
logP
:float
orlist/ndarray
offloat
orNone
-
- For case (a): Typically None. If the PubChem search returns logP, it’s stored automatically.
- For case (b,c): user can supply. If given, stored in self.logP as a numpy array.
db
:instance
ofCompoundIndex
orsimilar
, optional-
- If you want to perform a PubChem search (case a) automatically, pass an instance.
- If omitted or None, no search is attempted, even if name is given.
raiseerror
:bool (default=True)
, optional- Raise an error if name is not found
Advanced Parameters
Property models from MigrationPropertyModels can be directly attached to the substance. Based on the current version of migration.py two models are proposed: - Set a diffusivity model using - Dmodel="model name" default ="Piringer" - Dtemplate=template dict coding for the key:value parameters (e.g, to bed used Diringer(key1=value1…)) note: the template needs to be valid (do not use None) default = {"polymer":None, "M":None, "T":None} - Set a Henry-like model using - kmodel="model name" default =None - ktemplate=template dict coding for the key:value parameters default = {"Pi":1.41, "Pk":3.97, "Vi":124.1, "Vk":30.9, "ispolymer":True, "alpha":0.14, "lngmin":0.0,"Psat":1.0} other models could be implemented in the future, read the module property.py for details.
Example of usage of Dpiringer m = migrant(name='limonene') # without the helper function Dvalue = m.D.evaluate(**dict(m.Dtemplate,polymer="LDPE",T=60)) # with the helper function Dvalue = m.Deval(polymer="LDPE",T=60)
Raises
ValueError if insufficient arguments are provided for any scenario.
Expand source code
class migrant: """ A class representing a migrating chemical substance. It can be initialized in three main ways: 1) Case (a) - By a textual name/CAS only (for a real compound search): --------------------------------------------------------- Example: m = migrant(name="anisole", db=my_compound_index) # or m = migrant(name="anisole", db=my_compound_index, M=None) In this mode: • A lookup is performed using db.find(name), which may return one or more records. • If multiple records match, data from each record is merged: - compound = The text used in the query (e.g. "anisole") - name = Concatenation of all distinct names from the search results - CAS = Concatenation of all CAS numbers from the search results - M = The minimum of all found molecular weights, stored in self.M (a numpy array also keeps the full set) - formula = The first formula - logP = All logP values concatenated into a numpy array (self.logP_array). The main attribute self.logP will be the same array or you may pick a single representative. 2) Case (b) - By numeric molecular weight(s) alone (generic substance): --------------------------------------------------------- Example: m = migrant(M=200) m = migrant(M=[100, 500]) # Possibly a range In this mode: • No search is performed. • name = "generic" (unless you override it). • compound = "single molecular weight" if 1 entry in M, or "list of molecular weights ranging from X to Y" if multiple. • CAS = None • M = the minimum of all provided M values (also stored in a numpy array) • logP = None by default, or can be supplied explicitly as an array 3) Case (c) - Name + numeric M/logP => Surrogate / hypothetical: --------------------------------------------------------- Example: m = migrant(name="mySurrogate", M=[200, 250], logP=[2.5, 3.0]) or m = migrant(name="surrogate", M=200) In this mode: • No lookup is performed. This is a “fake” compound not found in PubChem. • compound = "single molecular weight" or "list of molecular weights ranging from X to Y" if multiple. • name = whatever user provides • CAS = None • M = min of the provided M array, stored in a numpy array • logP = user-provided array or single float, stored in a numpy array Attributes ---------- compound : str For case (a) => the search text; For case (b,c) => textual description of the numeric M array. name : str or list For case (a) => aggregated list of all found names (string-joined); For case (b) => "generic" or user-supplied name; For case (c) => user-supplied name. CAS : list or None For case (a) => aggregated CAS from search results; For case (b,c) => None. M : float The *minimum* M from either the search results or the user-supplied array. M_array : numpy.ndarray The full array of all M values found or provided. logP : float or numpy.ndarray or None For case (a) => an array of all logP from the search results (or None if not found); For case (b) => None or user-supplied value/array; For case (c) => user-supplied value/array. """ # class attribute, maximum width _maxdisplay = 40 # migrant constructor def __init__(self, name=None, M=None, logP=None, Dmodel = "Piringer", Dtemplate = {"polymer":"LLDPE", "M":50, "T":40}, # do not use None kmodel = "kFHP", ktemplate = {"Pi":1.41, "Pk":3.97, "Vi":124.1, "Vk":30.9, "ispolymer":True, "alpha":0.14, "lngmin":0.0,"Psat":1.0}, # do not use None db=dbdefault, raiseerror=True): """ Create a new migrant instance. Parameters ---------- name : str or None - A textual name for the substance to be looked up in PubChem (case a), or a custom name for a surrogate (case c). - If None, and M is given, we treat it as a numeric-only initialization (case b). M : float or list/ndarray of float or None - For case (a): If provided as None, we do a PubChem search by name. - For case (b): The numeric molecular weight(s). No search is performed if name is None. - For case (c): Combined name and numeric M => a surrogate with no search. logP : float or list/ndarray of float or None - For case (a): Typically None. If the PubChem search returns logP, it’s stored automatically. - For case (b,c): user can supply. If given, stored in self.logP as a numpy array. db : instance of CompoundIndex or similar, optional - If you want to perform a PubChem search (case a) automatically, pass an instance. - If omitted or None, no search is attempted, even if name is given. raiseerror : bool (default=True), optional Raise an error if name is not found Advanced Parameters ------------------- Property models from MigrationPropertyModels can be directly attached to the substance. Based on the current version of migration.py two models are proposed: - Set a diffusivity model using - Dmodel="model name" default ="Piringer" - Dtemplate=template dict coding for the key:value parameters (e.g, to bed used Diringer(key1=value1...)) note: the template needs to be valid (do not use None) default = {"polymer":None, "M":None, "T":None} - Set a Henry-like model using - kmodel="model name" default =None - ktemplate=template dict coding for the key:value parameters default = {"Pi":1.41, "Pk":3.97, "Vi":124.1, "Vk":30.9, "ispolymer":True, "alpha":0.14, "lngmin":0.0,"Psat":1.0} other models could be implemented in the future, read the module property.py for details. Example of usage of Dpiringer m = migrant(name='limonene') # without the helper function Dvalue = m.D.evaluate(**dict(m.Dtemplate,polymer="LDPE",T=60)) # with the helper function Dvalue = m.Deval(polymer="LDPE",T=60) Raises ------ ValueError if insufficient arguments are provided for any scenario. """ # local import # import implicity property migration models (e.g., Dpiringer) from patankar.property import MigrationPropertyModels, MigrationPropertyModel_validator self.compound = None # str self.name = None # str or list self.cid = None # int or list self.CAS = None # list or None self.M = None # float self.formula = None self.smiles = None self.M_array = None # np.ndarray self.logP = None # float / np.ndarray / None # special case if name==M==None: name = 'toluene' # Convert M to a numpy array if given if M is not None: if isinstance(M, (float, int)): M_array = np.array([float(M)], dtype=float) else: # Convert to array M_array = np.array(M, dtype=float) else: M_array = None # Similarly, convert logP to array if provided if logP is not None: if isinstance(logP, (float, int)): logP_array = np.array([float(logP)], dtype=float) else: logP_array = np.array(logP, dtype=float) else: logP_array = None # Case (a): name is provided, M=None => real compound lookup if (name is not None) and (M is None): if db is None: raise ValueError("A db instance is required for searching by name when M is None.") df = db.find(name, output_format="simple") if df.empty: if raiseerror: raise ValueError(f"<{name}> not found") print(f"LOADPUBCHEM ERRROR: <{name}> not found - empty object returned") # No record found self.compound = name self.name = [name] self.cid = [] self.CAS = [] self.M_array = np.array([], dtype=float) self.M = None self.formula = None self.smiles = None self.logP = None else: # Possibly multiple matching rows self.compound = name all_names = [] all_cid = [] all_cas = [] all_m = [] all_formulas = [] all_smiles = [] all_logP = [] for _, row in df.iterrows(): # Gather a list/set of names row_names = row.get("name", []) if isinstance(row_names, str): row_names = [row_names] row_syns = row.get("synonyms", []) combined_names = set(row_names) | set(row_syns) all_names.extend(list(combined_names)) # CID row_cid = row.get("CID", []) if row_cid: all_cid.append(row_cid) # CAS row_cas = row.get("CAS", []) if row_cas: all_cas.extend(row_cas) # M row_m = row.get("M", None) if row_m is not None: try: all_m.append(float(row_m)) except: all_m.append(np.nan) else: all_m.append(np.nan) # logP row_logp = row.get("logP", None) if row_logp not in (None, ""): try: all_logP.append(float(row_logp)) except: all_logP.append(np.nan) else: all_logP.append(np.nan) # formula (as a string) row_formula = row.get("formula", None) # Even if None, we append so the index lines up with M all_formulas.append(row_formula) # SMILES (as a string) row_smiles = row.get("SMILES", None) # Even if None, we append so the index lines up with M all_smiles.append(row_smiles) # Convert to arrays arr_m = np.array(all_m, dtype=float) arr_logp = np.array(all_logP, dtype=float) # Some dedup / cleaning unique_names = list(set(all_names)) unique_cid = list(set(all_cid)) unique_cas = list(set(all_cas)) # Store results in the migrant object self.name = unique_names self.cid = unique_cid[0] if len(unique_cid)==1 else unique_cid self.CAS = unique_cas if unique_cas else None self.M_array = arr_m # Minimum M if np.isnan(arr_m).all(): self.M = None self.formula = None self.smiles = None else: idx_min = np.nanargmin(arr_m) # index of min M self.M = arr_m[idx_min] # pick that M self.formula = all_formulas[idx_min] # pick formula from same record self.smiles = all_smiles[idx_min] # pick smilesfrom same record # Valid logP valid_logp = arr_logp[~np.isnan(arr_logp)] if valid_logp.size > 0: self.logP = valid_logp # or store as a list/mean/etc. else: self.logP = None # Case (b): name is None, M is provided => generic substance # ---------------------------------------------------------------- elif (name is None) and (M_array is not None): # No search performed if M_array.size == 1: self.compound = "single molecular weight" else: self.compound = (f"list of molecular weights ranging from " f"{float(np.min(M_array))} to {float(np.max(M_array))}") # name => "generic" or if user explicitly set name=..., handle it here self.name = "generic" # from instructions self.cid = None self.CAS = None self.M_array = M_array self.M = float(np.min(M_array)) self.formula = None self.smiles = None self.logP = logP_array # user-supplied or None # Case (c): name is not None and M is provided => surrogate # ---------------------------------------------------------------- elif (name is not None) and (M_array is not None): # No search is done, it doesn't exist in PubChem if M_array.size == 1: self.compound = "single molecular weight" else: self.compound = (f"list of molecular weights ranging from " f"{float(np.min(M_array))} to {float(np.max(M_array))}") self.name = name self.cid self.CAS = None self.M_array = M_array self.M = float(np.min(M_array)) self.formula = None self.smiles = None self.logP = logP_array else: # If none of these scenarios apply, user gave incomplete or conflicting args raise ValueError("Invalid arguments. Provide either name for search (case a), " "or M for a generic (case b), or both for a surrogate (case c).") # Model validation and paramameterization # ---------------------------------------- # Diffusivity model if Dmodel is not None: if not isinstance(Dmodel,str): raise TypeError(f"Dmodel should be str not a {type(Dmodel).__name__}") if Dmodel not in MigrationPropertyModels["D"]: raise ValueError(f'The diffusivity model "{Dmodel}" does not exist') Dmodelclass = MigrationPropertyModels["D"][Dmodel] if not MigrationPropertyModel_validator(Dmodelclass,Dmodel,"D"): raise TypeError(f'The diffusivity model "{Dmodel}" is corrupted') if Dtemplate is None: Dtemplate = {} if not isinstance(Dtemplate,dict): raise TypeError(f"Dtemplate should be a dict not a {type(Dtemplate).__name__}") self.D = Dmodelclass self.Dtemplate = Dtemplate.copy() self.Dtemplate.update(M=self.M,logP=self.logP) else: self.D = None self.Dtemplate = None # Henry-like model if kmodel is not None: if not isinstance(kmodel,str): raise TypeError(f"kmodel should be str not a {type(kmodel).__name__}") if kmodel not in MigrationPropertyModels["k"]: raise ValueError(f'The Henry-like model "{kmodel}" does not exist') kmodelclass = MigrationPropertyModels["k"][kmodel] if not MigrationPropertyModel_validator(kmodelclass,kmodel,"k"): raise TypeError(f'The Henry-like model "{kmodel}" is corrupted') if ktemplate is None: ktemplate = {} if not isinstance(ktemplate,dict): raise TypeError(f"ktemplate should be a dict not a {type(ktemplate).__name__}") self.k = kmodelclass self.ktemplate = ktemplate.copy() self.ktemplate.update(Pi=self.polarityindex,Vi=self.molarvolumeMiller) else: self.k = None self.ktemplate = None # helper property to combine D and Dtemplate @property def Deval(self): """Return a callable function that evaluates D with updated parameters.""" if self.D is None: return lambda **kwargs: None # Return a function that always returns None def func(**kwargs): updated_template = dict(self.Dtemplate, **kwargs) return self.D.evaluate(**updated_template) return func # helper property to combine k and ktemplate @property def keval(self): """Return a callable function that evaluates k with updated parameters.""" if self.k is None: return lambda **kwargs: None # Return a function that always returns None def func(**kwargs): updated_template = dict(self.ktemplate, **kwargs) return self.k.evaluate(**updated_template) return func def __repr__(self): """Formatted string representation summarizing key attributes.""" # Define header info = [f"<{self.__class__.__name__} object>"] # Collect attributes attributes = { "Compound": self.compound, "Name": self.name, "cid": self.cid, "CAS": self.CAS, "M (min)": self.M, "M_array": self.M_array if self.M_array is not None else "N/A", "formula": self.formula, "smiles": self.smiles if hasattr(self,"smiles") else "N/A", "logP": self.logP, "P' (calc)": self.polarityindex } if isinstance(self,migrantToxtree): attributes["Compound"] = self.ToxTree["IUPACTraditionalName"] attributes["Name"] = self.ToxTree["IUPACName"] attributes["Toxicology"] = self.CramerClass attributes["TTC"] = f"{self.TTC} {self.TTCunits}" attributes["CF TTC"] = f"{self.CFTTC} {self.CFTTCunits}" alerts = self.alerts # Process alerts alert_index = 0 for key, value in alerts.items(): if key.startswith("Alert") and key != "Alertscounter" and value.upper() == "YES": alert_index += 1 # Convert key name to readable format (split at capital letters) alert_text = ''.join([' ' + char if char.isupper() and i > 0 else char for i, char in enumerate(key)]) attributes[f"alert {alert_index}"] = alert_text.strip() # Remove leading space # Determine column width based on longest attribute name key_width = max(len(k) for k in attributes.keys()) + 2 # Add padding # Format attributes with indentation for key, value in attributes.items(): formatted_key = f"{key}:".rjust(key_width) formatted_value = self.dispmax(value) info.append(f" {formatted_key} {formatted_value}") # Print formatted representation repr_str = "\n".join(info) print(repr_str) # Return a short summary for interactive use return str(self) def __str__(self): """Formatted string representing the migrant""" onename = self.name[0] if isinstance(self.name,list) else self.name return f"<{self.__class__.__name__}: {self.dispmax(onename,16)} - M={self.M} g/mol>" def dispmax(self,content,maxwidth=None): """ optimize display """ strcontent = str(content) maxwidth = self._maxdisplay if maxwidth is None else min(maxwidth,self._maxdisplay) if len(strcontent)>maxwidth: nchar = round(maxwidth/2) return strcontent[:nchar]+" [...] "+strcontent[-nchar:] else: return content # calculated propeties (rough estimates) @property def polarityindex(self,logP=None,V=None): """ Computes the polarity index (P') of the compound. The polarity index (P') is derived from the compound's logP value and its molar volume V(), using an empirical (fitted) quadratic equation: E = logP * ln(10) - S P' = (-B - sqrt(B² - 4A(C - E))) / (2A) where: - S is the entropy contribution, calculated from molar volume. - A, B, C are empirical coefficients. Returns ------- float The estimated polarity index P' based on logP and molar volume. Notes ----- - For highly polar solvents (beyond water), P' saturates at **10.2**. - For extremely hydrophobic solvents (beyond n-Hexane), P' is **0**. - Accuracy is dependent on the reliability of logP and molar volume models. Example ------- >>> compound.polarityindex 8.34 # Example output """ return polarity_index(logP=self.logP if logP is None else logP, V=self.molarvolumeMiller if V is None else V) @property def molarvolumeMiller(self, a=0.997, b=1.03): """ Estimates molar volume using the Miller empirical model. The molar volume (V_m) is calculated based on molecular weight (M) using the empirical formula: V_m = a * M^b (cm³/mol) where: - `a = 0.997`, `b = 1.03` are empirically derived constants. - `M` is the molecular weight (g/mol). - `V_m` is the molar volume (cm³/mol). Returns ------- float Estimated molar volume in cm³/mol. Notes ----- - This is an approximate model and may not be accurate for all compounds. - Alternative models include the **Yalkowsky & Valvani method**. Example ------- >>> compound.molarvolumeMiller 130.5 # Example output """ return a * self.M**b @property def molarvolumeLinear(self): """ Estimates molar volume using a simple linear approximation. This method provides a rough estimate of molar volume, particularly useful for small to mid-sized non-ionic organic molecules. It is based on: V_m = 0.935 * M + 14.2 (cm³/mol) where: - `M` is the molecular weight (g/mol). - `V_m` is the estimated molar volume (cm³/mol). - Empirical coefficients are derived from **Yalkowsky & Valvani (1980s)**. Returns ------- float Estimated molar volume in cm³/mol. Notes ----- - This method is often *"okay"* for non-ionic organic compounds. - Accuracy decreases for very large, ionic, or highly branched molecules. - More precise alternatives include **Miller's model** or **group contribution methods**. Example ------- >>> compound.molarvolumeLinear 120.7 # Example output """ return 0.935 * self.M + 14.2
Subclasses
- patankar.loadpubchem.migrantToxtree
Instance variables
var Deval
-
Return a callable function that evaluates D with updated parameters.
Expand source code
@property def Deval(self): """Return a callable function that evaluates D with updated parameters.""" if self.D is None: return lambda **kwargs: None # Return a function that always returns None def func(**kwargs): updated_template = dict(self.Dtemplate, **kwargs) return self.D.evaluate(**updated_template) return func
var keval
-
Return a callable function that evaluates k with updated parameters.
Expand source code
@property def keval(self): """Return a callable function that evaluates k with updated parameters.""" if self.k is None: return lambda **kwargs: None # Return a function that always returns None def func(**kwargs): updated_template = dict(self.ktemplate, **kwargs) return self.k.evaluate(**updated_template) return func
var molarvolumeLinear
-
Estimates molar volume using a simple linear approximation.
This method provides a rough estimate of molar volume, particularly useful for small to mid-sized non-ionic organic molecules. It is based on:
V_m = 0.935 * M + 14.2 (cm³/mol)
where: -
M
is the molecular weight (g/mol). -V_m
is the estimated molar volume (cm³/mol). - Empirical coefficients are derived from Yalkowsky & Valvani (1980s).Returns
float
- Estimated molar volume in cm³/mol.
Notes
- This method is often "okay" for non-ionic organic compounds.
- Accuracy decreases for very large, ionic, or highly branched molecules.
- More precise alternatives include Miller's model or group contribution methods.
Example
>>> compound.molarvolumeLinear 120.7 # Example output
Expand source code
@property def molarvolumeLinear(self): """ Estimates molar volume using a simple linear approximation. This method provides a rough estimate of molar volume, particularly useful for small to mid-sized non-ionic organic molecules. It is based on: V_m = 0.935 * M + 14.2 (cm³/mol) where: - `M` is the molecular weight (g/mol). - `V_m` is the estimated molar volume (cm³/mol). - Empirical coefficients are derived from **Yalkowsky & Valvani (1980s)**. Returns ------- float Estimated molar volume in cm³/mol. Notes ----- - This method is often *"okay"* for non-ionic organic compounds. - Accuracy decreases for very large, ionic, or highly branched molecules. - More precise alternatives include **Miller's model** or **group contribution methods**. Example ------- >>> compound.molarvolumeLinear 120.7 # Example output """ return 0.935 * self.M + 14.2
var molarvolumeMiller
-
Estimates molar volume using the Miller empirical model.
The molar volume (V_m) is calculated based on molecular weight (M) using the empirical formula:
V_m = a * M^b (cm³/mol)
where: -
a = 0.997
,b = 1.03
are empirically derived constants. -M
is the molecular weight (g/mol). -V_m
is the molar volume (cm³/mol).Returns
float
- Estimated molar volume in cm³/mol.
Notes
- This is an approximate model and may not be accurate for all compounds.
- Alternative models include the Yalkowsky & Valvani method.
Example
>>> compound.molarvolumeMiller 130.5 # Example output
Expand source code
@property def molarvolumeMiller(self, a=0.997, b=1.03): """ Estimates molar volume using the Miller empirical model. The molar volume (V_m) is calculated based on molecular weight (M) using the empirical formula: V_m = a * M^b (cm³/mol) where: - `a = 0.997`, `b = 1.03` are empirically derived constants. - `M` is the molecular weight (g/mol). - `V_m` is the molar volume (cm³/mol). Returns ------- float Estimated molar volume in cm³/mol. Notes ----- - This is an approximate model and may not be accurate for all compounds. - Alternative models include the **Yalkowsky & Valvani method**. Example ------- >>> compound.molarvolumeMiller 130.5 # Example output """ return a * self.M**b
var polarityindex
-
Computes the polarity index (P') of the compound.
The polarity index (P') is derived from the compound's logP value and its molar volume V(), using an empirical (fitted) quadratic equation:
E = logP * ln(10) - S P' = (-B - sqrt(B² - 4A(C - E))) / (2A)
where: - S is the entropy contribution, calculated from molar volume. - A, B, C are empirical coefficients.
Returns
float
- The estimated polarity index P' based on logP and molar volume.
Notes
- For highly polar solvents (beyond water), P' saturates at 10.2.
- For extremely hydrophobic solvents (beyond n-Hexane), P' is 0.
- Accuracy is dependent on the reliability of logP and molar volume models.
Example
>>> compound.polarityindex 8.34 # Example output
Expand source code
@property def polarityindex(self,logP=None,V=None): """ Computes the polarity index (P') of the compound. The polarity index (P') is derived from the compound's logP value and its molar volume V(), using an empirical (fitted) quadratic equation: E = logP * ln(10) - S P' = (-B - sqrt(B² - 4A(C - E))) / (2A) where: - S is the entropy contribution, calculated from molar volume. - A, B, C are empirical coefficients. Returns ------- float The estimated polarity index P' based on logP and molar volume. Notes ----- - For highly polar solvents (beyond water), P' saturates at **10.2**. - For extremely hydrophobic solvents (beyond n-Hexane), P' is **0**. - Accuracy is dependent on the reliability of logP and molar volume models. Example ------- >>> compound.polarityindex 8.34 # Example output """ return polarity_index(logP=self.logP if logP is None else logP, V=self.molarvolumeMiller if V is None else V)
Methods
def dispmax(self, content, maxwidth=None)
-
optimize display
Expand source code
def dispmax(self,content,maxwidth=None): """ optimize display """ strcontent = str(content) maxwidth = self._maxdisplay if maxwidth is None else min(maxwidth,self._maxdisplay) if len(strcontent)>maxwidth: nchar = round(maxwidth/2) return strcontent[:nchar]+" [...] "+strcontent[-nchar:] else: return content
class oPP (l=4e-05, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in oPP', **extra)
-
extended pantankar.layer for bioriented polypropylene oPP
oPP layer constructor
Expand source code
class oPP(layer): """ extended pantankar.layer for bioriented polypropylene oPP """ _chemicalsubstance = "propylene" # monomer for polymers _polarityindex = 1.0 # Non-polar, but oriented film might have slight morphological differences def __init__(self, l=40e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in oPP",**extra): """ oPP layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="bioriented polypropylene", layercode="oPP", **extra ) def density(self, T=None): """ density of bioriented PP: density(T in K) Typically close to isotactic PP around ~910 kg/m^3. """ T = self.T if T is None else check_units(T, None, "degC")[0] return 910 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of bioriented PP """ return 0, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of bioriented PP
Expand source code
@property def Tg(self): """ glass transition temperature of bioriented PP """ return 0, "degC"
Methods
def density(self, T=None)
-
density of bioriented PP: density(T in K) Typically close to isotactic PP around ~910 kg/m^3.
Expand source code
def density(self, T=None): """ density of bioriented PP: density(T in K) Typically close to isotactic PP around ~910 kg/m^3. """ T = self.T if T is None else check_units(T, None, "degC")[0] return 910 * (1 - 3*(T - layer._defaults["Td"]) * 7e-5), "kg/m**3"
Inherited members
class plasticizedPVC (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in plasticized PVC', **extra)
-
extended pantankar.layer for plasticized PVC
plasticized PVC layer constructor
Expand source code
class plasticizedPVC(layer): """ extended pantankar.layer for plasticized PVC """ _chemicalsubstance = "vinyl chloride" # monomer for polymers _polarityindex = 4.5 # Plasticizers can slightly change overall polarity/solubility. def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in plasticized PVC",**extra): """ plasticized PVC layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="plasticized PVC", layercode="pPVC", **extra ) def density(self, T=None): """ density of plasticized PVC: ~1300 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1300 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of plasticized PVC """ return -40, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of plasticized PVC
Expand source code
@property def Tg(self): """ glass transition temperature of plasticized PVC """ return -40, "degC"
Methods
def density(self, T=None)
-
density of plasticized PVC: ~1300 kg/m^3
Expand source code
def density(self, T=None): """ density of plasticized PVC: ~1300 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1300 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
Inherited members
class rPET (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in rPET', **extra)
-
extended pantankar.layer for PET in its rubbery state (above ~76°C)
rubbery PET layer constructor
Expand source code
class rPET(layer): """ extended pantankar.layer for PET in its rubbery state (above ~76°C) """ _chemicalsubstance = "ethylene terephthalate" # monomer for polymers _polarityindex = 5.0 # Polyester with significant dipolar interactions (Ph = phenylene ring). def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in rPET",**extra): """ rubbery PET layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="rubbery PET", layercode="rPET", **extra ) def density(self, T=None): """ density of rubbery PET: ~1350 kg/m^3 but with a different expansion slope possible, if needed """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1350 * (1 - 3*(T - layer._defaults["Td"]) * 1e-4), "kg/m**3" @property def Tg(self): """ approximate glass transition temperature of PET """ return 76, "degC"
Ancestors
Instance variables
var Tg
-
approximate glass transition temperature of PET
Expand source code
@property def Tg(self): """ approximate glass transition temperature of PET """ return 76, "degC"
Methods
def density(self, T=None)
-
density of rubbery PET: ~1350 kg/m^3 but with a different expansion slope possible, if needed
Expand source code
def density(self, T=None): """ density of rubbery PET: ~1350 kg/m^3 but with a different expansion slope possible, if needed """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1350 * (1 - 3*(T - layer._defaults["Td"]) * 1e-4), "kg/m**3"
Inherited members
class rigidPVC (l=0.0002, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername='layer in rigid PVC', **extra)
-
extended pantankar.layer for rigid PVC
rigid PVC layer constructor
Expand source code
class rigidPVC(layer): """ extended pantankar.layer for rigid PVC """ _chemicalsubstance = "vinyl chloride" # monomer for polymers _polarityindex = 4.0 # Chlorine substituents give moderate polarity. def __init__(self, l=200e-6, D=1e-14, T=None, k=None, C0=None, lunit=None, Dunit=None, kunit=None, Cunit=None, layername="layer in rigid PVC",**extra): """ rigid PVC layer constructor """ super().__init__( l=l, D=D, k=k, C0=C0, T=T, lunit=lunit, Dunit=Dunit, kunit=kunit, Cunit=Cunit, layername=layername, layertype="polymer", layermaterial="rigid PVC", layercode="PVC", **extra ) def density(self, T=None): """ density of rigid PVC: ~1400 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1400 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3" @property def Tg(self): """ glass transition temperature of rigid PVC """ return 80, "degC"
Ancestors
Instance variables
var Tg
-
glass transition temperature of rigid PVC
Expand source code
@property def Tg(self): """ glass transition temperature of rigid PVC """ return 80, "degC"
Methods
def density(self, T=None)
-
density of rigid PVC: ~1400 kg/m^3
Expand source code
def density(self, T=None): """ density of rigid PVC: ~1400 kg/m^3 """ T = self.T if T is None else check_units(T, None, "degC")[0] return 1400 * (1 - 3*(T - layer._defaults["Td"]) * 5e-5), "kg/m**3"
Inherited members