# -*- coding: utf-8 -*-
"""
This module defines functions for performing isoconversional analysis.
The isoconversional methods function all starts with 'isoconversional_analysis'
"""
import numpy as np
from bisect import bisect_left
import scipy.optimize
import time as time_module
from tqdm import tqdm
import interpolation
from scipy import stats
[docs]
def check_boundaries(minimum, maximum, list_of_real_convs):
"""
Check if the specified conversion boundaries are within the valid range of the input data.
Parameters
----------
minimum : float
Minimum conversion level to check.
maximum : float
Maximum conversion level to check.
list_of_real_convs : list of numpy.ndarray
List of NumPy arrays containing conversion data for multiple experiments.
Returns
-------
None
Raises
------
ValueError
If the specified boundaries are outside the valid range of the input data.
"""
for conv_data in list_of_real_convs:
min_conv_data = np.min(conv_data)
max_conv_data = np.max(conv_data)
if minimum < min_conv_data:
raise ValueError(f"The minimum conversion you want to analyze ({minimum}) is lower than the minimum in the data ({min_conv_data}). Please increase the value of the minimum conversion.")
if maximum > max_conv_data:
raise ValueError(f"The maximum conversion you want to analyze ({maximum}) is higher than the maximum in the data ({max_conv_data}). Please lower the value of the maximum conversion.")
[docs]
def find_closest_value(target_value, sorted_array):
"""
Find the closest value to the target_value in a sorted NumPy array.
Parameters
----------
target_value : float
The value to find the closest value to.
sorted_array : numpy.ndarray
A sorted NumPy array of numbers.
Returns
-------
tuple: A tuple containing two elements:
- closest_index (int): The index of the closest value in the array.
- closest_value (float): The closest value in the array.
If two numbers are equally close, the smallest number is returned.
"""
if len(sorted_array) == 0:
raise ValueError("Input array must not be empty.")
# Find the index where target_value should be inserted in the sorted array
insert_index = bisect_left(sorted_array, target_value)
# If target_value is smaller than or equal to the first element of the array
if insert_index == 0:
return 0, sorted_array[0]
# If target_value is larger than or equal to the last element of the array
if insert_index == len(sorted_array):
return len(sorted_array) - 1, sorted_array[-1]
# Calculate the differences only once
prev_index = insert_index - 1
next_index = insert_index
prev_value, next_value = sorted_array[prev_index], sorted_array[next_index]
# Check which value is closer to target_value and return it
if next_value - target_value < target_value - prev_value:
return next_index, next_value
else:
return prev_index, prev_value
[docs]
def get_data_at_conversion(target_conversion, conversions, times, temperatures):
"""
Extract data before the target conversion for each dataset.
Parameters
----------
target_conversion : float
The target conversion value.
conversions : list of numpy.ndarray
List of NumPy arrays containing conversion data.
times : list of numpy.ndarray
List of NumPy arrays containing time data.
temperatures : list of numpy.ndarray
List of NumPy arrays containing temperature data.
Returns
-------
tuple
A tuple containing three lists:
- new_conversions (list of numpy.ndarray): List of NumPy arrays with data up to target_conversion.
- new_times (list of numpy.ndarray): List of NumPy arrays with corresponding time data.
- new_temperatures (list of numpy.ndarray): List of NumPy arrays with corresponding temperature data.
"""
new_conversions = []
new_times = []
new_temperatures = []
for i in range(len(conversions)):
# Find the index and value of the closest conversion value
index, value = find_closest_value(target_conversion, conversions[i])
# Append data up to and including the closest conversion value
new_conversions.append(conversions[i][:index+1])
new_times.append(times[i][:index+1])
new_temperatures.append(temperatures[i][:index+1])
return new_conversions, new_times, new_temperatures
[docs]
def get_index_area_of_calculation(conversion_lists, list_of_points_for_analysis):
"""
Return the indexes of the interval of calculation for the modified integral J.
Parameters
----------
conversion_lists : List
List with conversions at different heating rates (one list element for one heating rate).
list_of_points_for_analysis : List
List containing all the points of conversion at which the activation energy will be calculated.
Raises
------
ValueError
Raised if the starting point is smaller than the step.
Returns
-------
index_list : List
Return the indexes of the interval of calculation for the modified integral J
References
----------
[1] S. Vyazovkin, « Modification of the integral isoconversional method to account for variation in the activation energy », Journal of Computational Chemistry, vol. 22, nᵒ 2, p. 178‑183, 2001, doi: 10.1002/1096-987X(20010130)22:2<178::AID-JCC5>3.0.CO;2-#.
"""
index_list = []
# Find the maximum size of the arrays
max_size = max(arr.size for arr in conversion_lists)
# Pad arrays with NaN to ensure uniform size
padded_conversion_lists = [
np.concatenate([arr, np.full(max_size - arr.size, np.nan)]) if arr.size < max_size else arr
for arr in conversion_lists
]
conversions_array = np.array(padded_conversion_lists)
# Compute the difference between values of experimental conversions, ignoring NaNs
delta_alpha_exp = np.nanmax(conversions_array[:, 1:] - conversions_array[:, :-1], axis=1)
# Compute the minimum delta to use for analysis
min_delta_alpha_analysis = np.nanmax(delta_alpha_exp)
min_alpha_analysis = np.nanmax(conversions_array[:, 0]) + min_delta_alpha_analysis
delta_analysis = list_of_points_for_analysis[1] - list_of_points_for_analysis[0]
max_alpha_analysis = min(
np.nanmin(conversions_array[:, -1]),
list_of_points_for_analysis[0] + delta_analysis * len(list_of_points_for_analysis)
)
# Validations
if list_of_points_for_analysis[0] < min_alpha_analysis:
raise ValueError(
f"\nThe minimum conversion for your analysis points is too low.\n"
f"Current minimum conversion = {list_of_points_for_analysis[0]}\n"
f"Minimum conversion allowed = {min_alpha_analysis}\n"
f"Please increase the minimum conversion to analyze."
)
if list_of_points_for_analysis[-1] > max_alpha_analysis:
raise ValueError(
f"\nThe maximum conversion for your analysis points is too high.\n"
f"Current maximum conversion = {list_of_points_for_analysis[-1]}\n"
f"Maximum conversion allowed for {len(list_of_points_for_analysis)} points = {max_alpha_analysis}\n"
f"Please decrease the maximum conversion to analyze."
)
if delta_analysis < min_delta_alpha_analysis:
raise ValueError(
f"\nThe step between your analysis points is too high.\n"
f"Current step = {delta_analysis}\n"
f"Minimum step = {min_delta_alpha_analysis}\n"
f"Please reduce the number of points you want to analyze."
)
start_value = list_of_points_for_analysis[0] - delta_analysis
# Build the index list
for i, conversion_array in enumerate(conversion_lists):
index_of_start_value, _ = find_closest_value(start_value, conversion_array)
indices = [index_of_start_value]
for point in list_of_points_for_analysis:
index, _ = find_closest_value(point, conversion_array)
indices.append(index)
index_list.append(indices)
return index_list
[docs]
def get_data_in_interval(conversion_lists, time_lists, temperature_lists, list_of_points_for_analysis):
"""
Return the conversion, time and temperature lists for each heating and for each interval of calculation.
1st level of the list corresponds to heating rate
2nd level of the list corresponds to interval of calculation
Parameters
----------
conversion_lists : List
List with conversions at different heating rates (one list element for one heating rate).
time_lists : List
List with times at different heating rates (one list element for one heating rate)
temperature_lists : List
List with temperatures at different heating rates (one list element for one heating rate)
list_of_points_for_analysis : List
List containing all the point of conversion at which the activation energy will be calculated
Returns
-------
new_list_of_conversions : List
List with conversions at different heating rates for different intervals
new_list_of_times : List
List with times at different heating rates for different intervals
new_list_of_temperatures : List
List with temperatures at different heating rates for different intervals
"""
new_list_of_conversions = []
new_list_of_times = []
new_list_of_temperatures = []
index_list = get_index_area_of_calculation(
conversion_lists, list_of_points_for_analysis)
for i in range(len(index_list)):
new_list_of_conversions.append([])
new_list_of_times.append([])
new_list_of_temperatures.append([])
for k in range(len(index_list[i])-1):
new_list_of_conversions[i].append(
conversion_lists[i][index_list[i][k]:index_list[i][k+1]])
new_list_of_times[i].append(
time_lists[i][index_list[i][k]:index_list[i][k+1]])
new_list_of_temperatures[i].append(
temperature_lists[i][index_list[i][k]:index_list[i][k+1]])
return new_list_of_conversions, new_list_of_times, new_list_of_temperatures
[docs]
def compute_integral_J(Ea, time_array, temperature_array):
r"""
Compute the integral described in Vyazovkin's paper.
Parameters
----------
Ea : float
Activation energy for the reaction.
time_array : numpy.ndarray
NumPy array of time intervals for the DSC scan.
temperature_array : numpy.ndarray
NumPy array of temperatures during the DSC scan.
Returns
-------
float
The computed integral value.
Notes
-----
The integral is computed using the following formula:
.. math::
J = \int_{t_0}^{t_f} e^{-\frac{E_a}{R\cdot T(t)}} \cdot dt
where:
- J is the computed integral value.
- Ea is the activation energy for the reaction.
- R is the universal gas constant (8.314 J/(mol·K)).
- T(t) is the temperature at time t during the DSC scan.
- :math:`t_0` and :math:`t_f` are the initial and final times in the time_array.
"""
R = 8.314 # Universal gas constant
# Calculate the average temperature between adjacent time points
avg_temperatures = (temperature_array[:-1] + temperature_array[1:]) / 2
# Calculate the differences in time
time_diff = np.diff(time_array)
# Calculate the contributions using the Arrhenius equation
contributions = np.exp(-Ea / (R * avg_temperatures)) * time_diff
# Compute the integral using the trapezoidal rule
integral_value = np.sum(contributions)
return integral_value
[docs]
def compute_function_to_minimize(Ea, time_arrays, temperature_arrays):
r"""
Compute the function to minimize described in Vyazovkin's paper.
Parameters
----------
Ea : float
Activation energy for which the function will be computed.
time_arrays : list of numpy.ndarray
List of NumPy arrays containing time data for multiple experiments.
temperature_arrays : list of numpy.ndarray
List of NumPy arrays containing temperature data for multiple experiments.
Returns
-------
float
Value of the function for the given activation energy.
Notes
-----
The function to minimize is computed using the following formula:
.. math::
F(E_a) = \sum_{i=1}^{N} \sum_{k=1, k \neq i}^{N} \frac{J_i}{J_k}
where:
- :math:`F(E_a)` is the computed function to minimize.
- N is the number of experiments.
- :math:`J_i` and :math:`J_k` are the integrals computed using the Vyazovkin formula for experiments i and k.
"""
num_experiments = len(time_arrays)
# Calculate integrals for all experiments and store them in integrals_J list
integrals_J = [compute_integral_J(Ea, time_arrays[i], temperature_arrays[i]) for i in range(num_experiments)]
# Create a 2D array to store ratios of integrals
ratios = np.zeros((num_experiments, num_experiments))
# Calculate the ratios of integrals using NumPy broadcasting
for i in range(num_experiments):
for k in range(num_experiments):
if i != k:
ratios[i, k] = integrals_J[i] / integrals_J[k]
# Sum all ratios to compute the function to minimize
func_sum = np.sum(ratios)
return func_sum
[docs]
def isoconversional_analysis_vyazovkin_method(conv_lists, time_lists, temperature_lists, initial_guess, min_conv, max_conv, number_of_points):
"""
Find the energy of activation for multiple conversions using the Vyazovkin method with a BFGS optimization algorithm.
Parameters
----------
conv_lists : list of numpy.ndarray
List containing a list of evolution of conversion for various heating rates (n, m) with n the number of heating rates and m the number of conversion points.
time_lists : list of numpy.ndarray
List containing a list of evolution of time for various heating rates (n, m) with n the number of heating rates and m the number of time points.
temperature_lists : list of numpy.ndarray
List containing a list of evolution of temperature for various heating rates (n, m) with n the number of heating rates and m the number of temperature points.
initial_guess : float
Initial guess of the activation energy for the first desired conversion.
min_conv : float
Minimum conversion at which the activation energy will be computed.
max_conv : float
Maximum conversion at which the activation energy will be computed.
number_of_points : int
Number of conversion points at which the activation energy will be computed.
Returns
-------
conv_values : numpy.ndarray
Array containing conversion points at which activation energy was assessed.
Ea_calculated : numpy.ndarray
Array containing assessed activation energy.
Notes
-----
It is crucial to provide interpolated data based on the conversion for stability. Without interpolation,
this method may produce unreliable results.
References
----------
[1] S. Vyazovkin, « Evaluation of activation energy of thermally stimulated solid-state reactions under arbitrary variation of temperature », Journal of Computational Chemistry, vol. 18, nᵒ 3, p. 393‑402, 1997, doi: 10.1002/(SICI)1096-987X(199702)18:3<393::AID-JCC9>3.0.CO;2-P.
"""
t_start = time_module.process_time()
# Check boundaries and data integrity
check_boundaries(min_conv, max_conv, conv_lists)
print("Progress of optimization:")
# Create an array of conversion levels to sample
conv_values = np.linspace(min_conv, max_conv, int(number_of_points))
# Create an empty array to store the activation energies obtained after optimization
Ea_calculated = np.empty(int(number_of_points))
# Initialize initial_guess
current_guess = initial_guess
# Iterates through conv_values while displaying a progress bar
for i, conv_value in enumerate(tqdm(conv_values, ncols=100)):
# Extract conversion, time, and temperature required to compute the integrals J
conv, time_, temperature = get_data_at_conversion(conv_value, conv_lists, time_lists, temperature_lists)
# Perform minimization of function described in the article using BFGS method
result = scipy.optimize.minimize(compute_function_to_minimize, x0=current_guess, args=(time_, temperature), method='BFGS', options={'gtol': 1e-15})
# Store the calculated activation energy
Ea_calculated[i] = result.x
# Update the current_guess for the next optimization step
current_guess = result.x
t_stop = time_module.process_time()
print("Done! It took", t_stop - t_start, "s for the optimization to be performed")
return conv_values, Ea_calculated
[docs]
def isoconversional_analysis_advanced_vyazovkin_method(conv_lists, time_lists, temperature_lists, initial_guess, min_conv, max_conv, number_of_points):
"""
Find the energy of activation for multiple conversions using the advanced Vyazovkin method with a Broyden-Fletcher-Goldfarb-Shanno algorithm to find the optimal Ea.
Parameters
----------
conv_lists : list of numpy.ndarray
List containing a list of evolution of conversion for various heating rates (n, m) with n the number of heating rates and m the number of conversion points.
time_lists : list of numpy.ndarray
List containing a list of evolution of time for various heating rates (n, m) with n the number of heating rates and m the number of time points.
temperature_lists : list of numpy.ndarray
List containing a list of evolution of temperature for various heating rates (n, m) with n the number of heating rates and m the number of temperature points.
initial_guess : float
Initial guess of the activation energy for the first desired conversion.
min_conv : float
Minimum conversion at which the activation energy will be computed.
max_conv : float
Maximum conversion at which the activation energy will be computed.
number_of_points : int
Number of conversion points at which the activation energy will be computed.
Returns
-------
conv_values : numpy.ndarray
Array containing conversion points at which activation energy was assessed.
Ea_calculated : list
List containing assessed activation energy.
average_time_at_calculation_point : list
List containing the average time of all heating programs at the conversion points at which activation energy was assessed.
average_temperature_at_calculation_point : list
List containing the average temperature of all heating programs at the conversion points at which activation energy was assessed.
Notes
-----
It is crucial to provide interpolated data based on the conversion for stability. Without interpolation,
this method may produce unreliable results.
References
----------
[1] S. Vyazovkin, « Modification of the integral isoconversional method to account for variation in the activation energy », Journal of Computational Chemistry, vol. 22, nᵒ 2, p. 178‑183, 2001, doi: 10.1002/1096-987X(20010130)22:2<178::AID-JCC5>3.0.CO;2-#.
"""
t_start = time_module.process_time()
# Check boundaries and data integrity
check_boundaries(min_conv, max_conv, conv_lists)
print("Progress of optimization:")
conv_values = np.linspace(min_conv, max_conv, int(number_of_points))
Ea_calculated = []
average_time_at_calculation_point = []
average_temperature_at_calculation_point = []
# Separate data into intervals
conversions_intervals, times_intervals, temperatures_intervals = get_data_in_interval(conv_lists, time_lists, temperature_lists, conv_values)
for i in tqdm(range(len(conv_values)), ncols=100):
# Get the experimental data for the computation of the function to minimize
times = [heating_rate[i] for heating_rate in times_intervals]
temperatures = [heating_rate[i] for heating_rate in temperatures_intervals]
result = scipy.optimize.minimize(compute_function_to_minimize, x0=initial_guess, args=(
times, temperatures), method='BFGS', options={'gtol': 1e-15})
Ea_calculated.append(float(result.x))
initial_guess = float(result.x)
t_stop = time_module.process_time()
# Calculate average time and temperature at each calculation point
for i in range(len(temperatures_intervals[0])):
time_sum = 0
temp_sum = 0
for k in range(len(temperatures_intervals)):
time_sum += times_intervals[k][i][-1]
temp_sum += temperatures_intervals[k][i][-1]
average_time_at_calculation_point.append(time_sum / len(temperatures_intervals))
average_temperature_at_calculation_point.append(temp_sum / len(temperatures_intervals))
print("Done! It took ", t_stop - t_start, "s for the optimization to be performed")
return conv_values, Ea_calculated, average_time_at_calculation_point, average_temperature_at_calculation_point
[docs]
def isoconversional_analysis_friedman_method(conv_lists, rate_lists, temperature_lists, min_conv, max_conv, number_of_points):
"""
Parameters
----------
conv_lists : list of numpy.ndarray
List containing a list of evolution of conversion for various heating rates (n, m) with n the number of heating rates and m the number of conversion points.
rate_lists : list of numpy.ndarray
List containing a list of evolution of temperature for various heating rates (n, m) with n the number of heating rates and m the number of temperature points.
temperature_lists : list of numpy.ndarray
List containing a list of evolution of temperature for various heating rates (n, m) with n the number of heating rates and m the number of temperature points.
min_conv : Float
Minimum conversion at which the activation energy will be computed.
max_conv : Float
Maximum conversion at which the activation energy will be computed.
number_of_points : Float
Number of conversion points at which the activation energy will be computed.
Returns
-------
conv_list : List
List containing conversion points at which activation energy was assessed.
Ea_calculated : List
List containing assessed activation energy.
Intercept : List
List containing the intercept from the linear regressions
of ln(dx/dt) as function of 1/T.
References
----------
[1] N. Sbirrazzuoli, « Is the Friedman Method Applicable to Transformations with Temperature Dependent Reaction Heat? », Macromolecular Chemistry and Physics, vol. 208, nᵒ 14, p. 1592‑1597, 2007, doi: 10.1002/macp.200700100.
"""
t_start = time_module.process_time()
# Check boundaries and data integrity
check_boundaries(min_conv, max_conv, conv_lists)
print("Progress of optimization:")
conv_list = np.linspace(min_conv, max_conv, int(number_of_points)) #Creates the list of conversion points at which the activation energy will be computed
Ea_calculated = []
intercept=[]
for i in tqdm(range(len(conv_list)), ncols=100):
one_over_T=[]
rate=[]
for k in range(len(conv_lists)):
index,value=find_closest_value(conv_list[i], conv_lists[k])
one_over_T.append(1/temperature_lists[k][index])
rate.append(rate_lists[k][index])
result = stats.linregress(one_over_T, np.log(rate))
Ea_calculated.append(result.slope*(-8.314))
intercept.append(result.intercept)
t_stop = time_module.process_time()
print("Done! It took ", t_stop - t_start, "s for the analysis to be performed")
return conv_list, Ea_calculated, intercept
if __name__ == "__main__":
import kinetic_models as km
import matplotlib.pyplot as plt
import interpolation as interp
color_list = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red',
'tab:purple', 'tab:brown', 'tab:pink', 'tab:grey', 'tab:olive', 'tab:cyan']
linestyle = ['dashed', ]
marker = ['o']
number_of_points = 10000
time_list = np.linspace(0, 25, number_of_points) # 30 minutes
time_lists = [time_list, time_list, time_list, time_list]
temperatures = [np.linspace(293, 443, number_of_points), # 5°C/min
np.linspace(293, 593, number_of_points), # 10°C/min
np.linspace(293, 743, number_of_points), # 15°C/min
np.linspace(293, 893, number_of_points)] # 20°C/min
A1 = 1e10
E1 = 70000
A2 = 1e13
E2 = 85000
m = 0.45
n = 1
conversions = []
rates = []
for i in range(len(time_lists)):
extent, rate, _, _, _ = km.compute_extent_and_rate(time_lists[i],
temperatures[i],
rate_law=km.rate_for_kamal,
rate_law_args=(A1,E1,A2,E2,m,n))
conversions.append(extent)
rates.append(rate)
linear_conversions, linear_times, linear_temperatures, linear_rates = interp.linear_interpolation(conversions,time_lists,temperatures,rates,1000)
conv_friedman, Ea_friedman, _ = isoconversional_analysis_friedman_method(linear_conversions,
linear_rates,
linear_temperatures,
0.01,
0.99,
100)
conv_vyazovkin, Ea_vyazovkin = isoconversional_analysis_vyazovkin_method(linear_conversions,
linear_rates,
linear_temperatures,
50000,
0.01,
0.99,
100)
conv_advanced_vyazovkin, Ea_advanced_vyazovkin, _, _ = isoconversional_analysis_advanced_vyazovkin_method(linear_conversions,
linear_rates,
linear_temperatures,
50000,
0.01,
0.99,
250)
fig, ax = plt.subplots(figsize=(8,6))
for i in range(len(linear_times)):
ax.plot(linear_times[i],linear_rates[i])
ax.legend()
fig, ax = plt.subplots(figsize=(8,6))
ax.plot(conv_friedman,Ea_friedman,label="Friedman method")
ax.plot(conv_vyazovkin, Ea_vyazovkin, label="Vyazovkin method")
ax.plot(conv_advanced_vyazovkin, Ea_advanced_vyazovkin, label="Advanced Vyazovkin method")
ax.legend()