Strain & Stress fields

In this tutorial you will learn to measure liquid fraction and individual bubble radius from respectively phase-segmented (cleaned) and bubble segmented (no-edge) images.

The tutorial is divided in the following sections:

  • Import libraries

  • Quantification folders

  • Get familiar with the input data

  • Strain - Shape tensor

  • Strain - Texture tensor

  • Stress - Batchelor tensor

A) Import libraries

[1]:
from FoamQuant import *
import numpy as np
import skimage as ski
import os
import matplotlib.pyplot as plt; plt.rc('font', size=20)
from tifffile import imread
from scipy import ndimage
import pickle as pkl
import pandas as pd

B) Quantification folders

[2]:
# Processing folders names
Quant_Folder = ['Q3_Bubble_Prop','Q4_Topology','Q5_Texture','Q6_Stress']

# Create the quantification folders (where we are going to save our results)
for Pi in Quant_Folder:
    if  os.path.exists(Pi):
        print('path already exist:',Pi)
    else:
        print('Created folder:',Pi)
        os.mkdir(Pi)
path already exist: Q3_Bubble_Prop
path already exist: Q4_Topology
path already exist: Q5_Texture
path already exist: Q6_Stress

Let’s read the first bubble-segmented image of the series (with no bubble on the edges).

C) Get familiar with the input data

[3]:
# Name and directory of  the no-edge bubble segmented images
dirnoedge = 'P5_BubbleNoEdge/'
namenoedge = 'BubbleNoEdge_'

# Read the first image of the series
Lab = imread(dirnoedge+namenoedge+strindex(1, 3)+'.tiff')

# Since we are now looking at more bubbles let's create a "larger" random colormap: here 500 random colors
rcmap = RandomCmap(500, verbose=False)

# Show a 3D-cut view of the volume
Cut3D(Lab,
      nameaxes=['z','y','x'],
      cmap=rcmap,
      interpolation='nearest',
      figblocksize=4)
/gpfs/offline1/staff/tomograms/users/flosch/Rheometer_Jupyter/Jupy_FoamQuant/FoamQuant/Figure.py:90: UserWarning: The figure layout has changed to tight
  plt.tight_layout()
_images/Jupy_FoamQuant_strain_stress_7_1.png

D) Strain - Measured from the bubble region (shape tensor)

The shape strain tensor \(U_S\) is computed from the shape tensor \(S\). Each bubble is represented by a set of coordinates {\(\textbf{r}\)} of all the voxels defining its region in the image. From this set can be defined both the center of mass coordinates, \(\langle \textbf{r} \rangle\), and the shape tensor as

\(\textbf{S}=\langle(\textbf{r}-\langle \textbf{r} \rangle) \otimes (\textbf{r}-\langle \textbf{r} \rangle)\rangle^{1/2}\)

The strain tensors \(\textbf{U}_\textbf{S}\) is derived from \(\textbf{S}\) as the deviation from an isotropic shape \(\mathbf{S_0}\) state (a perfect sphere):

$:nbsphinx-math:textbf{U}_:nbsphinx-math:textbf{S} = \log`(:nbsphinx-math:textbf{S}`) - \log`(:nbsphinx-math:mathbf{S_0}`) $

with \(\mathbf{S_0} = S_0 \textbf{Id}\) and \(\textbf{Id}\) the identity tensor. The isotropic state is build from the three shape tensor eigenvalues \(\lambda_i\) as \(S_0 = (\lambda_1 \lambda_2 \lambda_3)^{1/3}\).

The shape tensor is already included in the saved properties by the RegionProp_Batch function.

[4]:
# Name and directory where we are going to save the bubble region properties
dir_Bubble_prop = 'Q3_Bubble_Prop/'
name_Bubble_prop = 'Bubble_Prop_'

# Indexes of the images of our time-series (we are working here with 10 subsequent images of the same foam sample, evolving over time).
imrange = [1,2,3,4,5,6,7,8,9,10]

RegionProp_Batch(namenoedge,
                 name_Bubble_prop,
                 dirnoedge,
                 dir_Bubble_prop,
                 imrange,
                 verbose=True,
                 endread='.tiff',
                 endsave='.tsv')
Path exist: True
Bubble_Prop_001: done
Bubble_Prop_002: done
Bubble_Prop_003: done
Bubble_Prop_004: done
Bubble_Prop_005: done
Bubble_Prop_006: done
Bubble_Prop_007: done
Bubble_Prop_008: done
Bubble_Prop_009: done
Bubble_Prop_010: done

Let’s open the first saved bubble-properties table with pandas.

[5]:
df = pd.read_csv(dir_Bubble_prop+name_Bubble_prop+strindex(1,n0=3)+'.tsv',sep = '\t')
display(df)
lab z y x vol rad area sph volfit S1 ... e2y e2x e3z e3y e3x U1 U2 U3 U type
0 1 15.286670 79.575571 183.687352 7622.0 12.208438 1889.990904 0.997457 7696.702997 11.356878 ... 12.123131 0.558382 -0.565461 0.558382 12.288122 -0.075555 0.016856 0.058699 0.118985 1
1 2 16.593385 130.636373 17.162980 9584.0 13.177087 2204.247246 0.996115 9674.503042 12.035502 ... 12.727413 -0.753596 -0.022763 -0.753596 12.968875 -0.093752 0.022430 0.071322 0.146864 1
2 3 15.635897 172.321484 101.120593 10324.0 13.507857 2309.744140 0.997574 10400.092065 12.805426 ... 13.886619 -0.036291 -0.224959 -0.036291 13.241960 -0.055850 -0.019896 0.075747 0.117809 -1
3 4 15.538057 175.370390 217.745069 9328.0 13.058701 2154.914928 0.997516 9371.282698 12.381921 ... 12.635434 -0.351062 0.549606 -0.351062 12.901907 -0.054760 -0.022566 0.077326 0.119294 -1
4 5 15.121800 200.194312 163.705468 10936.0 13.769663 2403.553261 0.997433 11037.743266 13.050254 ... 14.409886 0.621144 -0.254747 0.621144 13.336950 -0.056747 -0.021472 0.078219 0.121240 -1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
936 937 232.931714 209.884521 86.382444 9387.0 13.086175 2168.907873 0.998400 9475.291923 12.489027 ... 13.894511 -0.170001 -0.174207 -0.170001 12.962521 -0.049827 -0.009192 0.059018 0.095265 -1
937 938 234.656031 34.253836 50.915761 9841.0 13.293833 2235.291662 0.997166 9895.232739 12.680207 ... 12.997291 -0.130880 -0.529484 -0.130880 12.878073 -0.049090 -0.035787 0.084877 0.127836 -1
938 939 234.123875 156.617150 218.298716 9889.0 13.315411 2238.448309 0.998684 9938.866176 12.619337 ... 13.528568 -0.152264 0.059711 -0.152264 13.724473 -0.055368 0.015737 0.039632 0.085592 1
939 940 234.464956 224.364352 219.393250 9274.0 13.033453 2143.488520 0.998308 9307.919924 12.256650 ... 12.866831 -0.005062 0.036705 -0.005062 13.650618 -0.062668 0.017493 0.045175 0.097010 1
940 941 234.799707 24.502990 72.834127 8193.0 12.505991 1977.594288 0.999019 8257.341727 11.949345 ... 12.912100 0.136215 0.314278 0.136215 12.591211 -0.048139 0.014913 0.033226 0.073929 1

941 rows × 26 columns

Let’s keep the shape tensor tensor columns

[6]:
#Shape tensor
df[['e1z','e1y','e1x',
    'e2z','e2y','e2x',
    'e3z','e3y','e3x']]
[6]:
e1z e1y e1x e2z e2y e2x e3z e3y e3x
0 12.390694 0.206741 -0.565461 0.206741 12.123131 0.558382 -0.565461 0.558382 12.288122
1 14.053116 -0.391930 -0.022763 -0.391930 12.727413 -0.753596 -0.022763 -0.753596 12.968875
2 13.557547 0.846910 -0.224959 0.846910 13.886619 -0.036291 -0.224959 -0.036291 13.241960
3 13.761952 -0.256240 0.549606 -0.256240 12.635434 -0.351062 0.549606 -0.351062 12.901907
4 13.758225 -0.470152 -0.254747 -0.470152 14.409886 0.621144 -0.254747 0.621144 13.336950
... ... ... ... ... ... ... ... ... ...
936 12.564094 -0.058917 -0.174207 -0.058917 13.894511 -0.170001 -0.174207 -0.170001 12.962521
937 14.152835 0.463591 -0.529484 0.463591 12.997291 -0.130880 -0.529484 -0.130880 12.878073
938 12.792572 -0.396828 0.059711 -0.396828 13.528568 -0.152264 0.059711 -0.152264 13.724473
939 12.671146 0.502182 0.036705 0.502182 12.866831 -0.005062 0.036705 -0.005062 13.650618
940 12.135317 -0.106419 0.314278 -0.106419 12.912100 0.136215 0.314278 0.136215 12.591211

941 rows × 9 columns

Individual bubbles are often highly deformed in what looks like random deformations. In order to extract the strain field of the structure, without this local “noize”, we spatially average the shape of the bubble inside boxes. Only after averaging, the average strain is computed from the average shape tensor.

First, let’s reshape our table in form of two lists: a list for the centroid coordinates and one for the shape tensors.

[7]:
LCoord = np.asarray(df[['z','y','x']])

numpy_shape = np.asarray(df[['e1z','e1y','e1x',
                             'e2z','e2y','e2x',
                             'e3z','e3y','e3x']])
LShape=[]
for bbli in range(len(numpy_shape)):
    LShape.append(numpy_shape[bbli].reshape(3,3))

Now that we have the data in this format, one can perform a cartesian-grid average. Here we choose to have 5x5x5 boxes over the pixel coordinates [0,125] along z,y and x. We choose to return the result in a structured format.

[8]:
Grids, CoordGrd, ShapeAVG, ShapeSTD, ShapeCount = Grid_Tavg(LCoord,
                                                            LShape,
                                                            Range = [0,250,0,250,0,250],
                                                            N=[5,5,5],
                                                            NanFill=True, #<- if no bubble in a box, fill with a nan value
                                                            structured=True)
[9]:
# Averaged shape tensor in the fist [0,0,0] box
ShapeAVG[0][0][0]
[9]:
array([[13.0621894 ,  0.35516098, -0.34083759],
       [ 0.35516098, 12.82388882,  0.36474759],
       [-0.34083759,  0.36474759, 13.22112962]])
[10]:
# The associated strain tensor
USfromS(ShapeAVG[0][0][0])
[10]:
array([[ 0.00213077,  0.02782792, -0.02633693],
       [ 0.02782792, -0.01634475,  0.02838876],
       [-0.02633693,  0.02838876,  0.01421397]])

To have a bit more a feeling of this strain tensor field, one can use the following representation, using ellipses. If you wish to learn about this representation, have a look at the following paper: Three-dimensional liquid foam flow through a hopper resolved by fast X-ray microtomography. CutTensor3D work similarly than Cut3D. It represents the three orthogonal cuts of the tensorial space.

[11]:
CutTensor3D(Grids,
            CoordGrd,
            ShapeAVG,
            ShapeCount,
            scale_factor = 150,
            figblocksize=4,
            vmin=-0.1,
            vmax=0.1,
            Countmin=5, #<- shows a symbol if there is al least 5 bubble in the average box
            showscale=False,
            nameaxes=['z','y','x'],
            DeviatoricType=2, #<- 2 for dealing with shape: Shape -> Strain
            colorbarlab=r'$U_S$')
Normal min/max ax0 -0.014470684848635748 0.03758690547066675
Normal min/max ax1 -0.042390339076687135 0.012313103703006308
Normal min/max ax2 -0.03931547642670907 0.022374484108615564
_images/Jupy_FoamQuant_strain_stress_21_1.png

We can also do the same for the whole time-resolved data. Since we have only a small number of bubbles, it is going to smooth a bit the dentencies. In addition we can use ProjTensor3D, which average even more along the whole volume.

[12]:
# Take into acount the whole time serie data
LLCoord=[]
LLShape=[]
for imi in range(len(imrange)):
    df = pd.read_csv(dir_Bubble_prop+name_Bubble_prop+strindex(imrange[imi],n0=3)+'.tsv',sep = '\t')

    LCoord = np.asarray(df[['z','y','x']])

    numpy_shape = np.asarray(df[['e1z','e1y','e1x',
                                 'e2z','e2y','e2x',
                                 'e3z','e3y','e3x']])
    LShape=[]
    for bbli in range(len(numpy_shape)):
        LShape.append(numpy_shape[bbli].reshape(3,3))

    LLCoord.append(LCoord)
    LLShape.append(LShape)

Grids_2, CoordGrd_2, ShapeAVG_2, ShapeSTD_2, ShapeCount_2 = Grid_Tavg(np.concatenate(LLCoord),
                                                            np.concatenate(LLShape),
                                                            Range = [0,250,0,250,0,250],
                                                            N=[5,5,5],
                                                            NanFill=True,
                                                            verbose=False,
                                                            structured=True)
[13]:
ProjTensor3D(Grids_2,
            CoordGrd_2,
            ShapeAVG_2,
            ShapeCount_2,
            scale_factor = 150,
            figblocksize=4,
            vmin=-0.1,
            vmax=0.1,
            Countmin=5,
            showscale=False,
            nameaxes=['z','y','x'],
            DeviatoricType=2, #<- 2 for dealing with shape: Shape -> Strain
            colorbarlab=r'$U_S$')
Normal min/max ax0 0.0018644002701723728 0.02751263607400842
Normal min/max ax1 -0.025305090187731964 0.008156738035116365
Normal min/max ax2 -0.023519841267799376 0.010775489595964052
_images/Jupy_FoamQuant_strain_stress_24_1.png

E) Strain - Measured from the distance between neighbooring bubbles (texture tensor)

We can now do the exact same steps, extracting the strain field but working with the texture tensor.

Each bubble is also characterized by a set of link vectors {\(\mathbf{l}\)} between its center and the neighboring bubble centers. The texture tensor associated to each bubble is defined as

\(\textbf{M}=\langle \mathbf{l} \otimes \mathbf{l} \rangle\)

with \(\langle ... \rangle\) the average over the set of neighboring bubbles.

The strain tensor \(\textbf{U}_\textbf{M}\) is derived from \(\textbf{M}\) as the deviation from an isotropic texture \(\mathbf{M_0}\) state:

\(\textbf{U}_\textbf{M} = \frac{1}{2} \left( \log(\textbf{M}) - \log(\mathbf{M_0}) \right)\)

The isotropic texture \(\mathbf{M_0}\) is constructed in the same manner. It is important to note that the factor \(\frac{1}{2}\) accounts for the difference in dimensionality: the texture tensor has units of length squared, whereas the shape tensor has units of length. Note that the texture tensor tends to be less noisy than the shape tensor in dry foams, but more noisy in wet foams. This was attributed to the decrease in the average coordination number \(Z\) and area of contact \(A\) between the bubbles when the liquid fraction \(\phi_\ell\) increases.

Anyway, let’s get this texture tensor from our images. For this we have to start by extracting the contact topology between our bubble (which bubble is in contact with which). We use GetContacts_Batch and indicate that we just want the contact table save=’table’. This function is wrapping spam.label.contacts.labelledContacts from the SPAM toolbox spam.label.contacts.labelledContacts.

[14]:
# Name and directory where we are going to save the contact topology
dirbubbleseg = 'P4_BubbleSegmented/'
namebubbleseg = 'BubbleSeg_'

dirnoedge = 'P5_BubbleNoEdge/'
namenoedge = 'BubbleNoEdge_'

dir_Topo = 'Q4_Topology/'
name_Topo = 'Topology_'

GetContacts_Batch(namebubbleseg,
                  namenoedge,
                  name_Topo,
                  dirbubbleseg,
                  dirnoedge,
                  dir_Topo,
                  imrange,
                  verbose=True,
                  endread='.tiff',
                  endread_noedge='.tiff',
                  endsave='.tiff',
                  n0=3,
                  save='table', # <- save the topology table
                  maximumCoordinationNumber=20)
Path exist: True
Topology_001: done
Topology_002: done
Topology_003: done
Topology_004: done
Topology_005: done
Topology_006: done
Topology_007: done
Topology_008: done
Topology_009: done
Topology_010: done

The contact table is looking like this, for each central bubble, we get it’s coordination, and the label of all its neighbooring bubbles.

[15]:
df = pd.read_csv(dir_Topo+name_Topo+'table_'+strindex(1,n0=3)+'.tsv',sep = '\t')
display(df)
lab lab_noedge Z z y x lab1 cont1 lab2 cont2 ... lab16 cont16 lab17 cont17 lab18 cont18 lab19 cont19 lab20 cont20
0 1 -1 3 2.692405 2.768354 8.936709 10 6 153 318 ... 0 0 0 0 0 0 0 0 0 0
1 2 -1 3 3.463628 2.487876 34.752667 10 7 118 10 ... 0 0 0 0 0 0 0 0 0 0
2 3 -1 7 4.504314 6.833565 61.568884 4 1 118 16 ... 0 0 0 0 0 0 0 0 0 0
3 4 -1 6 4.723270 2.845283 88.178616 3 1 134 2 ... 0 0 0 0 0 0 0 0 0 0
4 5 -1 6 4.585279 6.343482 164.845108 6 3 9 8 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1657 1658 -1 9 244.488782 228.989763 43.207362 1491 8364 1504 8366 ... 0 0 0 0 0 0 0 0 0 0
1658 1659 -1 6 246.556267 238.599726 20.218664 1506 8529 1518 8559 ... 0 0 0 0 0 0 0 0 0 0
1659 1660 -1 5 244.988495 243.245025 224.386505 1449 8404 1513 8426 ... 0 0 0 0 0 0 0 0 0 0
1660 1661 -1 5 245.887428 247.547247 39.662284 1504 8455 1520 8531 ... 0 0 0 0 0 0 0 0 0 0
1661 1662 -1 2 247.550136 247.308943 246.818428 1513 8610 1576 8667 ... 0 0 0 0 0 0 0 0 0 0

1662 rows × 46 columns

This table is perfect for us who want to compute the texture for each individual central bubble. We can run Texture_Batch on the whole time series.

[16]:
dir_Texture = 'Q5_Texture/'
name_Texture = 'Texture_'

Texture_Batch(name_Topo+'table_',
              name_Texture,
              dir_Topo,
              dir_Texture,
              imrange,
              verbose=True,
              endsave='.tsv',
              n0=3)
Path exist: True
Texture_001: done
Texture_002: done
Texture_003: done
Texture_004: done
Texture_005: done
Texture_006: done
Texture_007: done
Texture_008: done
Texture_009: done
Texture_010: done

The texture table is looking like this, for each central bubble, the texture tensor is saved in as ‘e1z’,’e1y’,’e1x’, …

[17]:
df = pd.read_csv(dir_Texture+name_Texture+strindex(1,n0=3)+'.tsv',sep = '\t')
display(df)
lab labnoedge z y x rad M1 M2 M3 e1z ... e2y e2x e3z e3y e3x U1 U2 U3 U type
0 154 1 15.286670 79.575571 183.687352 13.244031 143.146698 188.665207 199.824185 145.088144 ... 199.761173 0.895467 -9.191918 0.895467 186.786772 -0.101612 0.036440 0.065172 0.154436 1.0
1 155 2 16.593385 130.636373 17.162980 13.412385 145.059897 173.953140 230.704676 166.649686 ... 224.300849 -16.785618 16.282702 -16.785618 158.767178 -0.107605 -0.016786 0.124391 0.202486 -1.0
2 156 3 15.635897 172.321484 101.120593 14.441475 176.124295 211.680976 243.314619 176.488827 ... 236.743360 -12.915726 -2.816887 -12.915726 217.887703 -0.084509 0.007436 0.077073 0.140379 1.0
3 157 4 15.538057 175.370390 217.745069 14.293863 159.800474 219.541577 243.110006 161.102709 ... 228.860442 -12.302243 5.740241 -12.302243 232.488906 -0.122867 0.035941 0.086927 0.189517 1.0
4 159 5 15.121800 200.194312 163.705468 13.813899 136.649861 182.360569 278.841570 138.356440 ... 268.554950 27.195228 -4.772531 27.195228 190.940610 -0.166964 -0.022682 0.189647 0.310703 -1.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
936 1512 937 232.931714 209.884521 86.382444 13.615627 154.233618 193.452225 213.536990 157.115806 ... 192.189452 -5.140028 1.909386 -5.140028 211.917575 -0.091984 0.021297 0.070687 0.144454 1.0
937 1514 938 234.656031 34.253836 50.915761 14.232325 174.557681 207.576403 229.370816 190.732572 ... 226.062002 1.767641 -16.343852 1.767641 194.710326 -0.074388 0.012234 0.062154 0.119665 1.0
938 1516 939 234.123875 156.617150 218.298716 14.098111 130.697754 235.697297 254.884119 153.221315 ... 232.118401 8.278592 6.961134 8.278592 235.939454 -0.209597 0.085233 0.124364 0.316217 1.0
939 1517 940 234.464956 224.364352 219.393250 13.813096 137.489777 204.724088 246.778179 158.126016 ... 210.912977 -14.217263 -41.189681 -14.217263 219.953051 -0.163842 0.035214 0.128628 0.258736 1.0
940 1521 941 234.799707 24.502990 72.834127 13.206825 129.720257 191.731632 213.348160 129.916408 ... 208.388919 9.135482 2.690941 9.135482 196.494723 -0.148044 0.047315 0.100729 0.226832 1.0

941 rows × 23 columns

As above, let’s reshape these table data in two lists, one for the coordinates, one for the texture tensors. Then, let’s average these in space and time in a 5x5x5 grid over [0,125] pixel range over the z,y and x coordinates.

[18]:
# Take into acount the whole time serie data
LLCoord=[]
LLTexture=[]
for imi in range(len(imrange)):
    df = pd.read_csv(dir_Texture+name_Texture+strindex(imrange[imi],n0=3)+'.tsv',sep = '\t')

    LCoord = np.asarray(df[['z','y','x']])

    numpy_texture = np.asarray(df[['e1z','e1y','e1x',
                                 'e2z','e2y','e2x',
                                 'e3z','e3y','e3x']])
    LTexture=[]
    for bbli in range(len(numpy_texture)):
        LTexture.append(numpy_texture[bbli].reshape(3,3))

    LLCoord.append(LCoord)
    LLTexture.append(LTexture)

Grids, CoordGrd, TextureAVG, TextureSTD, TextureCount = Grid_Tavg(np.concatenate(LLCoord),
                                                                  np.concatenate(LLTexture),
                                                                  Range = [0,250,0,250,0,250],
                                                                  N=[5,5,5],
                                                                  NanFill=True,
                                                                  verbose=False,
                                                                  structured=True)

Once again we can represent the obtained strain \(U_M\) by using the ProjTensor3D function.

[19]:
ProjTensor3D(Grids,
            CoordGrd,
            TextureAVG,
            TextureCount,
            scale_factor = 150,
            figblocksize=4,
            vmin=-0.1,
            vmax=0.1,
            Countmin=5,
            showscale=False,
            nameaxes=['z','y','x'],
            DeviatoricType=3, #<- 3 for dealing with shape: Texture -> Strain
            colorbarlab=r'$U_M$')
Normal min/max ax0 0.0023403027427819747 0.04642474069427069
Normal min/max ax1 -0.034138627203550244 0.020506781868971365
Normal min/max ax2 -0.025567206792855193 0.01330556953933863
_images/Jupy_FoamQuant_strain_stress_36_1.png

F) Stress - Measured from the interface curvature (Batchelor stress tensor)

The last but maybe one of the most interesting tool.

Batchelor established an expression for the elastic stress emerging from the local structure of a suspension of fluid particles in a continuous liquid phase, such as liquid foams and emulsions. The elastic stress tensor induced by the surface tension over all the interfaces bounding any given bubble takes the following form:

\(\sigma_{ij} = \frac{\Gamma}{V} \oint_S (\delta_{ij} - n_i n_j) dS\)

where \(V\) and \(S\) refer to the individual bubble volume and interface area, and \(\Gamma\) to the surface tension.

This stress tensor measure \(\sigma_{ij}\) was implemented in 3D and evaluated for each individual bubble by integrating over all liquid-gas interfaces surrounding it. The local stress measurement was validated via a tomo-rheoscopy setup under a quasi-static liquid foam flow regime Multiscale stress dynamics in sheared liquid foams revealed by tomo-rheoscopy.

The individual bubble stress tensor measure can be obtained batchwise with the function Batchelor_Batch. Each bubble interface is meshed thank’s to a scikit-image function mesh_surface_area. The tensor is integrated thank’s to a modified function form PoreSpy mesh_surface_area.

[20]:
dir_Stress = 'Q6_Stress/'
name_Stress = 'Stress_'

Batchelor_Batch(namenoedge,
                name_Stress,
                dirnoedge,
                dir_Stress,
                imrange,
                verbose=True,
                endread='.tiff',
                endsave='.tsv',
                n0=3)
Path exist: True
100%|██████████| 941/941 [02:16<00:00,  6.91it/s]
Stress_001: done
100%|██████████| 938/938 [02:13<00:00,  7.00it/s]
Stress_002: done
100%|██████████| 942/942 [02:13<00:00,  7.04it/s]
Stress_003: done
100%|██████████| 943/943 [02:13<00:00,  7.05it/s]
Stress_004: done
100%|██████████| 956/956 [02:17<00:00,  6.97it/s]
Stress_005: done
100%|██████████| 945/945 [02:13<00:00,  7.05it/s]
Stress_006: done
100%|██████████| 941/941 [02:15<00:00,  6.97it/s]
Stress_007: done
100%|██████████| 950/950 [02:15<00:00,  7.01it/s]
Stress_008: done
100%|██████████| 948/948 [02:15<00:00,  7.02it/s]
Stress_009: done
100%|██████████| 945/945 [02:14<00:00,  7.00it/s]
Stress_010: done

The stress tensor table is looking like this. The integral tensor term is saved as ‘B11’,’B12’,’B22’, … (1,2,3 for z,y,x). The ‘bii’ are the ‘Bii’ divided by the bubble volume \(V\). In order to obtain a physical result in \(Pa\) we multiply the ‘b’ tensor by the surface tension and divide it by the pixel size.

[21]:
df = pd.read_csv(dir_Stress+name_Stress+strindex(1,n0=3)+'.tsv',sep = '\t')
display(df)
lab z y x vol mesharea B11 B12 B13 B22 B23 B33 b11 b12 b13 b22 b23 b33
0 1 15.286670 79.575571 183.687352 7622.0 1959.851467 1311.222441 9.783312 -29.556724 1300.680149 32.160212 1307.800343 0.172031 0.001284 -0.003878 0.170648 0.004219 0.171582
1 2 16.593385 130.636373 17.162980 9584.0 2296.892631 1586.908722 -18.623406 3.384973 1496.785631 -50.586303 1510.090909 0.165579 -0.001943 0.000353 0.156175 -0.005278 0.157564
2 3 15.635897 172.321484 101.120593 10324.0 2388.240172 1592.128459 55.777156 -17.082450 1615.968433 3.985240 1568.383452 0.154216 0.005403 -0.001655 0.156525 0.000386 0.151916
3 4 15.538057 175.370390 217.745069 9328.0 2214.715346 1527.999650 -18.222971 35.719418 1441.040353 -26.531295 1460.390689 0.163808 -0.001954 0.003829 0.154485 -0.002844 0.156560
4 5 15.121800 200.194312 163.705468 10936.0 2483.656637 1662.292886 -43.144426 -15.439269 1706.953553 50.088335 1598.066836 0.152002 -0.003945 -0.001412 0.156086 0.004580 0.146129
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
936 937 232.931714 209.884521 86.382444 9387.0 2241.817678 1448.171000 -13.260882 -11.095406 1547.387234 -10.606871 1488.077121 0.154274 -0.001413 -0.001182 0.164844 -0.001130 0.158525
937 938 234.656031 34.253836 50.915761 9841.0 2288.534321 1587.043902 28.740443 -42.765422 1497.399214 -8.763997 1492.625525 0.161269 0.002920 -0.004346 0.152159 -0.000891 0.151674
938 939 234.123875 156.617150 218.298716 9889.0 2289.019896 1486.954152 -30.977424 9.202753 1532.810831 -12.427219 1558.274810 0.150364 -0.003133 0.000931 0.155002 -0.001257 0.157577
939 940 234.464956 224.364352 219.393250 9274.0 2195.087605 1427.199943 35.601312 3.485379 1451.899878 1.854403 1511.075388 0.153893 0.003839 0.000376 0.156556 0.000200 0.162937
940 941 234.799707 24.502990 72.834127 8193.0 2036.418182 1324.486919 -5.870227 16.283892 1381.072489 3.034954 1367.276955 0.161661 -0.000716 0.001988 0.168567 0.000370 0.166884

941 rows × 18 columns

As above, let’s reshape these table data in two lists, one for the coordinates, one for the stress tensors. Then, let’s average these in space and time in a 5x5x5 grid over [0,125] pixel range over the z,y and x coordinates.

[22]:
# Take into acount the whole time serie data
pixsize=2.75e-6#um
surftension=21.1e-3#N/m

LLCoord=[]
LLStress=[]
for imi in range(len(imrange)):
    df = pd.read_csv(dir_Stress+name_Stress+strindex(imrange[imi],n0=3)+'.tsv',sep = '\t')

    LCoord = np.asarray(df[['z','y','x']])

    numpy_stress = np.asarray(df[['b11','b12','b13',
                                 'b12','b22','b23',
                                 'b13','b23','b33']])*surftension/pixsize
    LStress=[]
    for bbli in range(len(numpy_stress)):
        LStress.append(numpy_stress[bbli].reshape(3,3))

    LLCoord.append(LCoord)
    LLStress.append(LStress)

Grids_sig, CoordGrd_sig, SigAVG, SigSTD, SigCount = Grid_Tavg(np.concatenate(LLCoord),
                                                              np.concatenate(LLStress),
                                                              Range = [0,250,0,250,0,250],
                                                              N=[5,5,5],
                                                              NanFill=True,
                                                              verbose=False,
                                                              structured=True)

Once again we can represent the obtained stress tensors \(\sigma\) by using the ProjTensor3D function.

[23]:
ProjTensor3D(Grids_sig,
            CoordGrd_sig,
            SigAVG,
            SigCount,
            scale_factor = 0.1,
            figblocksize=4,
            vmin=-40,
            vmax=40,
            Countmin=5,
            showscale=False,
            nameaxes=['z','y','x'],
            DeviatoricType=1, #<- 1 for dealing with stress: Full stress tensor -> Deviatoric stress tensor
            colorbarlab=r'$\sigma$ (Pa)')
Normal min/max ax0 -0.5432484576144363 20.837223257146253
Normal min/max ax1 -21.085453366830986 6.273667281737579
Normal min/max ax2 -17.612397861857293 9.448668331923121
_images/Jupy_FoamQuant_strain_stress_44_1.png

You have now completed this tutorial. I hope it has been helpfull to you. Go back to FoamQuant - Examples for more examples and tutorials.

[ ]: