Fitting single flow curve

This notebook contains a list scripts focusing on analysis and visualization of a single flow curve. A flow curve is the measurement of the viscosity as a function of shear rate. In a measurment a constant shear rate is imposed on a fluid sample and the required shear stress is measured.

Since for Non newtonian fluids the viscosity is a function of the shear rate, the flow curve measurement is a way to characterize such functional dependence.

Rheological models can be used to fit the flow curve and extract parameters that in some case directly relates to fundamental material properties.

In this notebook we provide some example of analysis of flow curves using powerful python libraries that we simply integrate to make flow curves analysis simple, easy to share and reproduce.

We provide a sequence of exaples structured as “recepies” each with a specific scope, plotting data, fitting data, evaluating confidence itervals…

Each recepy can be considered “stand alone” meaning that the only objects that cells share are the general imports in the first cell and the functions defined through the notebook. This should make it easy to copy and paste a cell in case you want to use it as a starting point for your own analysis.

If you have a google account you can run this documentation notebook Open in colab

[ ]:
#executing the notebook in colab requires installation of the rheofit library
!pip install git+https://github.com/marcocaggioni/rheofit.git
[ ]:
%matplotlib inline

import sys
sys.path.append("./../") #in case you are running the notebook in binder or from the cloned repository
import rheofit
import lmfit
import pybroom as pb
import matplotlib.pyplot as plt
import pandas as pd
import emcee
import corner
import seaborn

Getting Example flow curve data

To make demonstration simple we stored an example flow curve in the rheodata module. In the future we would like to collect flow curves from published works to make them easily available for training purpouse.

The data provided refer to a 75w% mineral oil in 11%LAS surfactant solution. The emusion droplets size ranges from 1-10 um and the measurement was performed with a Couette geometry at 40C.

The example_emulsion method returns a Pandas dataframe with column ‘Shear rate’ in [1/s] and column ‘Stress’ in [Pa]. The choice of the name convention is from the excel exported file by the TA Trios software. A possibly better choice could be the RheoML convention ‘ShearRate’ and ‘ShearStress’.

Example of importing data from different file formats are provided in a different Notebook, here we focus on possible analysis and visualization on a single flow curve.

[3]:
#using provided example data
example_data=rheofit.rheodata.example_emulsion()
example_data.head()
[3]:
Shear rate Stress
0 999.89475 131.22475
1 891.25100 121.57225
2 794.32825 112.56525
3 707.94600 104.25800
4 630.95750 96.58295

Plotting flow curve data

To plot the data we use matplolib. We define a simple function that plot the data, set x and y scale to log and the label for x and y. The function return a matplolib figure object that can be stored in a variable.

[4]:
data=rheofit.rheodata.example_emulsion()

def plot_data(data):
    plt.plot(data['Shear rate'],data['Stress'],'o')
    plt.yscale('log')
    plt.xscale('log')
    plt.ylabel('$\sigma [Pa]$');
    plt.xlabel('$\dot\gamma [1/s]$');

    return plt.gcf()

fig1=plot_data(data);
_images/Fitting_single_flow_curve_7_0.png

Fitting flow curve data to a rheological model

This is the most important functionality mostly provided by the LMFIT library. The rheofit module just provide the rheology model definition and documentation.

For example a popular model for yield stress fluids is the Hershel Bulckley model is a lmfit.Model object

[5]:
rheofit.models.HB_model
[5]:
<lmfit.Model: Model(HB, prefix='HB_')>

The model object is based on the function rheofit.models.HB

[6]:
help(rheofit.models.HB)
Help on function HB in module rheofit.models:

HB(x, ystress=1.0, K=1.0, n=0.5)
    Hershel-Bulkley Model

    Note:

    .. math::
       \sigma= \sigma_y + K \cdot \dot\gamma^n

    Args:
        ystress: yield stress [Pa]

        K : Consistency index [Pa s^n]

        n : Shear thinning index []

    Returns:
        stress : Shear Stress, [Pa]

To fit the data we use the fit method of the model object passing the stress, the shear rate and the weights

[7]:
data=rheofit.rheodata.example_emulsion()
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))
res_fit
[7]:

Model

Model(HB, prefix='HB_')

Fit Statistics

fitting methodleastsq
# function evals29
# data points101
# variables3
chi-square 0.52363806
reduced chi-square 0.00534325
Akaike info crit.-525.469582
Bayesian info crit.-517.624221

Variables

name value standard error relative error initial value min max vary
HB_ystress 3.18629490 0.06296913 (1.98%) 1.0 0.00000000 inf True
HB_K 2.56281495 0.09118889 (3.56%) 1.0 0.00000000 inf True
HB_n 0.53928815 0.00752288 (1.39%) 0.5 0.00000000 1.00000000 True

Correlations (unreported correlations are < 0.100)

HB_KHB_n-0.9380
HB_ystressHB_K-0.7366
HB_ystressHB_n0.6757

Plotting Fit results

The fit method returns a fit result object that contains all the information on the data, the model used and the results

[8]:
data=rheofit.rheodata.example_emulsion()
data=data[data['Shear rate']>0.1]
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']*0.1))

kwarg={'xscale':'log','yscale':('log')}
res_fit.plot_fit(ax_kws=kwarg,yerr=False);
plt.figure()
res_fit.plot_residuals(yerr=False,ax_kws={'xscale':'log'})
[8]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fed09a0c8d0>
_images/Fitting_single_flow_curve_15_1.png
_images/Fitting_single_flow_curve_15_2.png
[9]:
data=rheofit.rheodata.example_emulsion()
data=data[data['Shear rate']>0.1]

model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))

def plot_fit(res_fit, xlist=None):
    if xlist is None:
        xlist=res_fit.userkws['x']

    plt.plot(res_fit.userkws['x'], res_fit.data,'o',label='Data')
    plt.plot(xlist, res_fit.eval(x=xlist),label='Best fit')
    plt.yscale('log')
    plt.xscale('log')
    plt.ylabel('$\sigma [Pa]$')
    plt.xlabel('$\dot\gamma [1/s]$')

plot_fit(res_fit);
_images/Fitting_single_flow_curve_16_0.png

Explore fit results with pybroom

Exploring the lmfit.fit_result object is not intuitive, using the convenient pybroom library make things very easy, especially when we explore lists of results from different sample.

[10]:
data=rheofit.rheodata.example_emulsion()
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))

pb.glance(res_fit)
[10]:
model method num_params num_data_points chisqr redchi AIC BIC num_func_eval success message
0 Model(HB, prefix='HB_') leastsq 3 101 0.523638 0.005343 -525.469582 -517.624221 29 True Fit succeeded.
[11]:
data=rheofit.rheodata.example_emulsion()
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))

pb.tidy(res_fit)
[11]:
name value min max vary expr stderr init_value
0 HB_K 2.562815 0 inf True NaN 0.091189 1.0
1 HB_n 0.539288 0 1.0 True NaN 0.007523 0.5
2 HB_ystress 3.186295 0 inf True NaN 0.062969 1.0
[12]:
data=rheofit.rheodata.example_emulsion()
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))

augmented=pb.augment(res_fit)
augmented.head()
[12]:
x data best_fit residual
0 999.89475 131.22475 109.492113 -0.165614
1 891.25100 121.57225 103.098212 -0.151959
2 794.32825 112.56525 97.083545 -0.137535
3 707.94600 104.25800 91.430975 -0.123032
4 630.95750 96.58295 86.118672 -0.108345

Plot residuals

[13]:
data=rheofit.rheodata.example_emulsion()
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))


def plot_residuals(res_fit):
    augmented=pb.augment(res_fit)
    plt.plot('x','residual',data=augmented)
    plt.xscale('log')
    plt.ylabel('residual - relative deviation');
    plt.xlabel('$\dot\gamma [1/s]$');

    return plt.gcf()

plot_residuals(res_fit);
_images/Fitting_single_flow_curve_22_0.png

Confidence interval

[14]:
data=rheofit.rheodata.example_emulsion()
data=data[data['Shear rate']>0.1]
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))

def plot_confidence(res_fit,expand=10):
    dely = res_fit.eval_uncertainty(x=res_fit.userkws['x'],sigma=3)*expand

    plt.plot(res_fit.userkws['x'], res_fit.data,'o',color='black',label='Data',markersize=5)
    plt.plot(res_fit.userkws['x'], res_fit.best_fit,label='Best fit TC model',color='red')
    plt.fill_between(res_fit.userkws['x'], res_fit.best_fit-dely,res_fit.best_fit+dely,
                     color='blue',alpha=0.2,label='0.9973 Confidence interval')
    plt.yscale('log')
    plt.xscale('log')
    plt.ylabel('$\sigma [Pa]$')
    plt.xlabel('$\dot\gamma [1/s]$')

    return plt.gcf()

plot_confidence(res_fit);
_images/Fitting_single_flow_curve_24_0.png

Fit specific shear rate range

[15]:
data=rheofit.rheodata.example_emulsion()
data=data[data['Shear rate']>0.1]
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))

min_shear_rate=0.1
max_shear_rate=1000

def fit_range(res_fit,min_shear_rate=0.1,max_shear_rate=100):
    data=pd.DataFrame.from_dict({'Shear rate':res_fit.userkws['x'],'Stress':res_fit.data})
    mask=(data['Shear rate']>=min_shear_rate) & (data['Shear rate']<=max_shear_rate)
    res_fit=model.fit(data[mask]['Stress'],x=data[mask]['Shear rate'],weights=1/(data[mask]['Stress']))

    return res_fit

plot_fit(res_fit);
plot_fit(fit_range(res_fit));
_images/Fitting_single_flow_curve_26_0.png
[16]:
data=rheofit.rheodata.example_emulsion()
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))

min_shear_rate=0.1
max_shear_rate=1000

def fit_range(res_fit,min_shear_rate=0.1,max_shear_rate=100):
    data=pd.DataFrame.from_dict({'Shear rate':res_fit.userkws['x'],'Stress':res_fit.data})
    mask=(data['Shear rate']>min_shear_rate) & (data['Shear rate']<max_shear_rate)
    res_fit=model.fit(data[mask]['Stress'],x=data[mask]['Shear rate'],weights=1/(data[mask]['Stress']))

    return res_fit

display(pb.tidy(fit_range(res_fit,min_shear_rate=0.1,max_shear_rate=10)))
display(pb.tidy(fit_range(res_fit,min_shear_rate=0.1,max_shear_rate=100)))
display(pb.tidy(fit_range(res_fit,min_shear_rate=0.1,max_shear_rate=1000)))
name value min max vary expr stderr init_value
0 HB_K 2.946032 0 inf True NaN 0.033507 1.0
1 HB_n 0.452852 0 1.0 True NaN 0.004298 0.5
2 HB_ystress 3.219395 0 inf True NaN 0.028909 1.0
name value min max vary expr stderr init_value
0 HB_K 2.498040 0 inf True NaN 0.037762 1.0
1 HB_n 0.517414 0 1.0 True NaN 0.003954 0.5
2 HB_ystress 3.589250 0 inf True NaN 0.034026 1.0
name value min max vary expr stderr init_value
0 HB_K 1.880492 0 inf True NaN 0.051488 1.0
1 HB_n 0.595807 0 1.0 True NaN 0.005270 0.5
2 HB_ystress 4.088222 0 inf True NaN 0.059816 1.0

Reduced \(\chi^2\) sensitivity on shear rate range

[17]:
data=rheofit.rheodata.example_emulsion()
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))


def explore_redchi(res_fit,min_shear_rate=0.01,max_shear_rate=1000):
    data=pd.DataFrame.from_dict({'Shear rate':res_fit.userkws['x'],'Stress':res_fit.data})
    res_dict={min_shear:fit_range(res_fit,min_shear_rate=min_shear,max_shear_rate=1000)
              for min_shear in data['Shear rate'][10:-1]}

    ax1= plt.subplot(2,1,1)

    plt.plot(data['Shear rate'],data['Stress'],'o')
    plt.yscale('log')
    plt.xscale('log')
    plt.xlabel('$\dot\gamma [1/s]$')
    plt.ylabel('$\sigma [Pa]$')

    ax2= plt.subplot(2,1,2,sharex = ax1)

    plt.plot(list(res_dict.keys()),pb.glance(res_dict)['redchi'])
    plt.xscale('log')
    plt.ylabel('$\chi_{red}$')
    plt.xlabel('$\dot\gamma_{min} [1/s]$')

    return plt.gcf()

explore_redchi(res_fit);
_images/Fitting_single_flow_curve_29_0.png

Model parameter sensitivity on shear rate range

[18]:
data=rheofit.rheodata.example_emulsion()
data.sort_values('Shear rate',ascending=False, inplace=True)

model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))


def explore_param_keephigh(res_fit,min_shear_rate=0.01,max_shear_rate=1000,param_name=None):
    '''keep the right range of data for the fit
    answer the question: how important it is how low we extend the analysis at low shear?
    '''
    data=pd.DataFrame.from_dict({'Shear rate':res_fit.userkws['x'],'Stress':res_fit.data})
    data.sort_values('Shear rate',ascending=False, inplace=True)
    res_fit=res_fit.model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))
    res_fit=fit_range(res_fit,min_shear_rate=min_shear_rate,max_shear_rate=max_shear_rate)
    res_dict={min_shear:fit_range(res_fit,min_shear_rate=min_shear,max_shear_rate=max(res_fit.userkws['x']))
              for min_shear in res_fit.userkws['x'][5:-1]}

    ax1= plt.subplot(2,1,1)

    plt.plot(data['Shear rate'],data['Stress'],'o')
    plt.yscale('log')
    plt.xscale('log')
    plt.xlabel('$\dot\gamma [1/s]$')
    plt.ylabel('$\sigma [Pa]$')

    xlim=plt.gca().get_xlim()
    plt.axvspan(xlim[0],min_shear_rate,color='gray',alpha=0.5)
    plt.axvspan(max_shear_rate,xlim[1],color='gray',alpha=0.5)

    ax2= plt.subplot(2,1,2,sharex = ax1)

    plt.plot(list(res_dict.keys()),pb.tidy(res_dict)['value'][pb.tidy(res_dict)['name']=='HB_n'])
    plt.xscale('log')
    plt.ylabel('$'+param_name+'$')
    plt.xlabel('$\dot\gamma_{min} [1/s]$')

    plt.axvspan(xlim[0],min_shear_rate,color='gray',alpha=0.5)
    plt.axvspan(max_shear_rate,xlim[1],color='gray',alpha=0.5)

    plt.gca().set_xlim(xlim)

    return plt.gcf()

explore_param_keephigh(res_fit,min_shear_rate=0.1,param_name='n');
_images/Fitting_single_flow_curve_31_0.png
[19]:
data=rheofit.rheodata.example_emulsion()
data.sort_values('Shear rate',ascending=False, inplace=True)

model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))


def explore_param_keeplow(res_fit,min_shear_rate=0.01,max_shear_rate=1001,param_name=None):
    '''keep the right range of data for the fit
    answer the question: how important it is how low we extend the analysis at low shear?
    '''

    data=pd.DataFrame.from_dict({'Shear rate':res_fit.userkws['x'],'Stress':res_fit.data})
    data.sort_values('Shear rate',ascending=True, inplace=True)
    res_fit=res_fit.model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']))
    res_fit=fit_range(res_fit,min_shear_rate=min_shear_rate,max_shear_rate=max_shear_rate)
    res_dict={max_shear:fit_range(res_fit,min_shear_rate=min(res_fit.userkws['x']),max_shear_rate=max_shear)
              for max_shear in res_fit.userkws['x'][5:-1]}

    ax1= plt.subplot(2,1,1)

    plt.plot(data['Shear rate'],data['Stress'],'o')
    plt.yscale('log')
    plt.xscale('log')
    plt.xlabel('$\dot\gamma [1/s]$')
    plt.ylabel('$\sigma [Pa]$')

    xlim=plt.gca().get_xlim()
    plt.axvspan(xlim[0],min_shear_rate,color='gray',alpha=0.5)
    plt.axvspan(max_shear_rate,xlim[1],color='gray',alpha=0.5)

    ax2= plt.subplot(2,1,2,sharex = ax1)

    plt.plot(list(res_dict.keys()),pb.tidy(res_dict)['value'][pb.tidy(res_dict)['name']=='HB_n'])
    plt.xscale('log')
    plt.ylabel('$'+param_name+'$')
    plt.xlabel('$\dot\gamma_{max} [1/s]$')

    plt.axvspan(xlim[0],min_shear_rate,color='gray',alpha=0.5)
    plt.axvspan(max_shear_rate,xlim[1],color='gray',alpha=0.5)

    plt.gca().set_xlim(xlim)

    return plt.gcf()

explore_param_keeplow(res_fit,min_shear_rate=0.1,param_name='n');
_images/Fitting_single_flow_curve_32_0.png

Emcee

[20]:
data=rheofit.rheodata.example_emulsion()
data=data[data['Shear rate']>0.1]
model=rheofit.models.HB_model
res_fit=model.fit(data['Stress'],x=data['Shear rate'],weights=1/(data['Stress']*0.1))
res_emcee=res_fit.emcee(steps=1000, nwalkers=50, burn=300)

corner.corner(res_emcee.flatchain, labels=res_emcee.var_names, truths=[res_fit.params[key].value for key in res_fit.params.keys()]);
display(res_fit)
/usr/local/lib/python3.6/dist-packages/emcee/ensemble.py:258: RuntimeWarning: Initial state is not linearly independent and it will not allow a full exploration of parameter space
  category=RuntimeWarning,
100%|██████████| 1000/1000 [01:23<00:00, 12.12it/s]

Model

Model(HB, prefix='HB_')

Fit Statistics

fitting methodleastsq
# function evals22
# data points80
# variables3
chi-square 10.8588639
reduced chi-square 0.14102421
Akaike info crit.-153.763595
Bayesian info crit.-146.617515

Variables

name value standard error relative error initial value min max vary
HB_ystress 4.08822217 0.05981584 (1.46%) 1.0 0.00000000 inf True
HB_K 1.88049246 0.05148762 (2.74%) 1.0 0.00000000 inf True
HB_n 0.59580716 0.00527012 (0.88%) 0.5 0.00000000 1.00000000 True

Correlations (unreported correlations are < 0.100)

HB_KHB_n-0.9655
HB_ystressHB_K-0.8080
HB_ystressHB_n0.7536
_images/Fitting_single_flow_curve_34_2.png