Click or drag to resize

Multifit Model Example

Below, an example implementation of a RelaxISPlugin_MultifitModel is shown.

The function of the plugin is chosen for illustrative purposes.

Requirements
  • Reference to the RelaxIS_SDK.dll

  • .NET Framework 4.7.2 target (class library)

  • Recommended: Development environment with compiler e.g. RelaxIS SDK Code Editor, Microsoft Visual Studio

Demonstrates

This examples illustrates the implementation of a MultifitModel plugin.

The plugin fits an RP model to multiple spectra by calculation the resistance value from an Arrhenius function.

Example
C#
// <copyright file="MyMultifitModel.cs" company="rhd instruments GmbH and Co. KG">
// Copyright (c) rhd instruments GmbH and Co. KG. All rights reserved.
// Licensed under the MIT No Attribution (MIT-0) license. See section 'License' in the 'SDK Examples / Tutorials' topic for full license information.
// </copyright>

namespace RelaxIS_SDK_Examples.Plugins
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using RelaxIS_SDK.Common;
    using RelaxIS_SDK.libMath;
    using RelaxIS_SDK.Plugins;

    /***
     * MultifitModels are a special type of fitting model used by the Multifit-function of RelaxIS.
     * It is used to fit not just one but multiple spectra simultaneously to one combined model.
     * The model can use spectras metadata as additional values for the calculation.
     * The model can have parameters that apply to all spectra equally and parameters individual for each spectrum.
     *
     * A good example (also implemented here) is to determine one of the parameters via an
     * Arrhenius-function. That way, the model has one set of Arrhenius parameters that is used to
     * calculate this value for each of the spectra depending on the Temperature metadata field.
     * This implicitely forces e.g. a bulk resistance parameter to adhere strictly to Arrhenius
     * behavior, instead of moving the Arrhenius fit into a separate step after the individual spectrum fits.
     */

    /// <summary>
    /// Defines an example <see cref="RelaxISPlugin_MultifitModel"/> class.
    /// </summary>
    public class MyMultifitModel
        : RelaxISPlugin_MultifitModel
    {
        /// <summary>
        /// Defines the metadata required on spectra for this plugin to be available.
        /// </summary>
        private static readonly string[] Metadata = new[] { MetadataNames.TEMPERATURE };

        /***
         * First, implement the default plugin properties Name and Description that describe the plugin.
         * This is mainly used for display purposes.
         */

        /// <inheritdoc/>
        public override string Name
        {
            get { return "MyMultifitModel"; }
        }

        /// <inheritdoc/>
        public override string Description
        {
            get { return "Implements a MultiFit model that calculates the series resistance of an RP model based on an Arrhenius function"; }
        }

        /***
         * Implement properties that tell RelaxIS which metadata is required and which transfer function should be used.
         */

        /// <inheritdoc/>
        public override List<string> RequiredMetadata
        {
            get
            {
                return new List<string>(Metadata);
            }
        }

        /// <inheritdoc/>
        public override string TransferFunction
        {
            get
            {
                // Refer to the RelaxIS settings -> Plugins window for the internal names of the transfer functions.
                return "Impedance";
            }
        }

        /***
         * This function gets a set of individual parameters for a specific spectrum. In most cases all spectra have the same set of
         * individual parameters, but that is not a requirement. Given that the resistance of the RP model in this example is
         * a shared parameter (see below), the individual parameters are just the CPE parameters.
         * Note, that this function also determines minimal and maxima, as well as initial values for the parameters.
         * This is helpful to make initialization of the parameters easier in the user interface, since these need to be initialized
         * manually for each of the spectra! Having a good automatic initialization algorithm is therefore very helpful.
         */

        /// <inheritdoc/>
        public override List<Fitparameter> GetIndividualParameters(ImpedanceSpectrum Spectrum, List<ImpedanceSpectrum> AllSpectra)
        {
            // Calculate the complex capacitances from the data
            var caps = Spectrum.Data.Select(p => 1.0 / (new Complex(0, 2.0 * Math.PI * p.Impedance.Frequency) * new Complex(p.Impedance.Real, p.Impedance.Imaginary)));

            // Take the minimum and maximum real capacitances and scale them by a factor for the new limits
            var cAvg = caps.Average(c => c.Real);
            var cMin = caps.Min(c => c.Real) / 10000;
            var cMax = caps.Max(c => c.Real) * 10000;

            // Individual parameters are a CPE Q and alpha for each spectrum
            var res = new List<Fitparameter>()
            {
                new Fitparameter("Q", false, cAvg, 0, cMin, cMax),
                new Fitparameter("alpha", false, 0.95, 0, 0.3, 1),
            };

            return res;
        }

        /***
         * This function defines the shared parameters. These will only appear once in the fit model calculation and hence apply to all spectra.
         * In this example these are the Arrhenius parameters that will be used to calculate a series resistance value.
         * This function also initializes the values via a linear fit to the logarithmic average real part of the spectra vs. the inverse temperature.
         */

        /// <inheritdoc/>
        public override List<Fitparameter> GetSharedParameters(List<ImpedanceSpectrum> Spectra)
        {
            // Select inverse temperature
            var temperatures = Spectra.Select(spectrum => 1.0 / spectrum.Metadata.Where(m => m.Name == "Temperature").First().Value).ToList();

            // Select log of average real parts of each spectrum
            var resistances = Spectra.Select(spectrum => spectrum.Data.Select(d => Math.Log(d.Impedance.Real)).Average()).ToList();

            // R = A*Exp(-Ea/(RT)) => ln(R) = ln(A) - Ea/(RT)
            // Linear fit: Ea = -Slope * R, A = Exp(Intercept)
            var reg = GetLinearRegression(temperatures, resistances);
            var ea = 1e-4;
            var ar = 100.0;
            if (!double.IsNaN(reg.Item1) && !double.IsNaN(reg.Item2))
            {
                ea = -reg.Item1 * 8.314;
                ar = Math.Exp(reg.Item2);
            }

            // Arrhenius pre-exponential factor and activation energy is shared across all spectra.
            var res = new List<Fitparameter>()
            {
                new Fitparameter("A_R", false, ar, 0, 1e-15, 1e15),
                new Fitparameter("Ea", false, ea, 0, 1e-15, 1e15),
            };

            return res;
        }

        /***
         * Now the actual fit function needs to be implemented.
         * The X[] parameters contain the frequency in index 0, the spectrum index at index 1, and the metadata as defined in the
         * RequiredMetadata in the subsequent indizes. Here we therefore find the temperature at index 2.
         * The Parameters[] argument contains the full list of parameters: First the shared parameters in the order defined in
         * GetSharedParameters(), and then the individual parameters as returned by GetIndividualParameters() for each spectrum.
         * The order of spectra is the same over all function. If GetIndividualParameters() returned the same number of parameters for
         * each spectrum, the GetIndividualParameterIndex() helper function can be used to determine the parameter indizes of a spectrums
         * parameters in the overall lists.
         * If spectra have different numbers of individual parameters, you can alternatively use the ParameterStartIndizes property that is
         * populated by RelaxIS during the setup phase.
         */

        /// <inheritdoc/>
        public override Complex FitFunction(double[] X, double[] Parameters)
        {
            var f = X[0];
            var iw = new Complex(0, 2 * Math.PI * f);
            var spectrumIdx = (int)X[1];
            var t = X[2];
            var ar = Parameters[0];
            var ea = Parameters[1];

            // Determine the parameters of the individual parameters

            // Given that all spectra have 2 individual parameters we can use the helper function:
            var qIdx = this.GetIndividualParameterIndex(2, 2, 0, spectrumIdx);
            var aIdx = this.GetIndividualParameterIndex(2, 2, 1, spectrumIdx);

            // Alternativly the ParameterStartIndizes dictionary can be used:
            var startIdx = this.ParameterStartIndizes[spectrumIdx];
            qIdx = startIdx + 0;
            aIdx = startIdx + 1;

            var q = Parameters[qIdx];
            var a = Parameters[aIdx];

            // Calculate R using the Arrhenius function
            var r = ar * Math.Exp(-ea / (8.314 * t));

            // Calculate the CPE value normally.
            var cpe = 1.0 / (Complex.Pow(iw, a) * q);
            return r + cpe;
        }

        /***
         * The PerformSetup function is called by RelaxIS before the GetIndividualParameters and GetSharedParameters functions are called and
         * can be used to e.g. perform initialization functions or show a user dialog with the given parent window.
         * Setting Cancel to true will stop the fit execution.
         */

        /// <inheritdoc/>
        public override void PerformSetup(IEnumerable<ImpedanceSpectrum> Spectra, IWin32Window parent, ref bool Cancel)
        {
            var count = Spectra.Count();
            var sb = new StringBuilder();
            sb.AppendLine("You could do some setup steps here that run before the parameter initialization step.");
            sb.AppendFormat("You have selected {0} spectra for fitting", count);
            sb.AppendFormat("This will result in a total of 2 shared and {0} individual fit parameters", count * 2);
            MessageBox.Show(parent, sb.ToString(), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        /***
         * The AfterInitialization function is called by RelaxIS after the GetIndividualParameters and GetSharedParameters functions are called and
         * can be used to e.g. perform post-initialization functions such as saving settings or to show a user dialog with the given parent window.
         * Setting Cancel to true will stop the fit execution.
         */

        /// <inheritdoc/>
        public override void AfterInitialization(IEnumerable<ImpedanceSpectrum> Spectra, IWin32Window parent, List<Fitparameter> SharedParameters, List<Tuple<ImpedanceSpectrum, List<Fitparameter>>> IndividualParameters, ref bool Cancel)
        {
            var sb = new StringBuilder();
            sb.AppendLine("You could do some parameter validation here and possibly cancel the fitting process.");
            MessageBox.Show(parent, sb.ToString(), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        /***
         * This function is called after the fit has completed, allowing the calculation of additional values from the fit results.
         */

        /// <inheritdoc/>
        public override Dictionary<string, double> GetAdditionalEvaluations(ImpedanceSpectrum Spectrum, List<ImpedanceSpectrum> AllSpectra, List<Fitparameter> SharedParameters, List<Fitparameter> IndividualParameters)
        {
            // With SI units the activation energy would be in J/mol
            var ea = SharedParameters[1].Value;
            var ev = ea * 6.2415093433e+18 / 6.022e23;
            var res = new Dictionary<string, double>
            {
                { "Ea [kJ/mol]", ea / 1000.0 },
                { "Ea [eV]", ev },
            };
            return res;
        }

        /***
         * Helper function to perform a linear least squares fit.
         */

        /// <summary>
        /// Calculate the unweighted linear regression over the data.
        /// </summary>
        /// <param name="x">The x (independent) values.</param>
        /// <param name="y">The y (dependent values).</param>
        /// <returns>The regression result, Slope | Intercept.</returns>
        private static Tuple<double, double> GetLinearRegression(IList<double> x, IList<double> y)
        {
            if (x.Count != y.Count)
            {
                throw new ArgumentException("The data lists have different length");
            }

            var xAvg = x.Average();
            var yAvg = y.Average();
            double numerator, denominator;
            numerator = 0;
            denominator = 0;
            var dCount = x.Count;
            for (int i = 0; i < dCount; i++)
            {
                var dx = x[i] - xAvg;
                numerator += dx * (y[i] - yAvg);
                denominator += dx * dx;
            }

            double slope;
            bool nan = false;
            if (numerator != 0 & denominator == 0)
            {
                slope = 0;
            }
            else if (numerator == 0 & denominator == 0)
            {
                nan = true;
                slope = double.PositiveInfinity;
            }
            else
            {
                slope = numerator / denominator;
            }

            if (!nan)
            {
                double b = yAvg - (slope * xAvg);
                return Tuple.Create(slope, b);
            }
            else
            {
                return Tuple.Create(double.NaN, double.NaN);
            }
        }
    }
}
See Also