Select Git revision
basicmath.cpp 14.69 KiB
//SPDX-License-Identifier: LGPL-3.0-or-later
//
// eisgenerator - a shared library and application to generate EIS spectra
// Copyright (C) 2022-2024 Carl Philipp Klemm <carl@uvos.xyz>
//
// This file is part of eisgenerator.
//
// eisgenerator is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// eisgenerator is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with eisgenerator. If not, see <http://www.gnu.org/licenses/>.
//
#include "basicmath.h"
#include <algorithm>
#include <random>
#include <limits>
#include <stdexcept>
#include "log.h"
#include "linearregession.h"
static size_t gradIndex(size_t dataSize, size_t inputIndex)
{
if(inputIndex == 0)
inputIndex = 1;
else if(inputIndex > dataSize-2)
inputIndex = dataSize-2;
return inputIndex;
}
std::complex<fvalue> eis::absGrad(const std::vector<eis::DataPoint>& data, size_t index)
{
if(data.size() < 3)
return std::complex<fvalue>(1,1);
index = gradIndex(data.size(), index);
return std::complex<fvalue>(std::abs((data[index+1].im.real()-data[index-1].im.real())/(data[index+1].omega-data[index-1].omega)),
std::abs((data[index+1].im.imag()-data[index-1].im.imag())/(data[index+1].omega-data[index-1].omega)));
}
void eis::mulitplyAdd(std::vector<eis::DataPoint>& data, fvalue mult, fvalue add)
{
for(eis::DataPoint& point : data)
point.im = point.im*mult + add;
}
fvalue eis::grad(const std::vector<fvalue>& data, const std::vector<fvalue>& omega, size_t index)
{
assert(data.size() == omega.size());
if(data.size() < 3)
return 0;
index = gradIndex(data.size(), index);
return (data[index+1]-data[index-1])/(omega[index+1]-omega[index-1]);
}
std::complex<fvalue> eis::grad(const std::vector<eis::DataPoint>& data, size_t index)
{
if(data.size() < 3)
return std::complex<fvalue>(1,1);
index = gradIndex(data.size(), index);
return std::complex<fvalue>((data[index+1].im.real()-data[index-1].im.real())/(data[index+1].omega-data[index-1].omega),
(data[index+1].im.imag()-data[index-1].im.imag())/(data[index+1].omega-data[index-1].omega));
}
std::complex<fvalue> eis::mean(const std::vector<eis::DataPoint>& data)
{
fvalue accumRe = 0;
fvalue accumIm = 0;
for(const eis::DataPoint& point : data)
{
accumRe += point.im.real();
accumIm += point.im.imag();
}
accumRe /= data.size();
accumIm /= data.size();
return std::complex<fvalue>(accumRe, accumIm);
}
static inline fvalue medianTrampoline(const std::vector<fvalue> data)
{
return eis::median(data);
}
std::complex<fvalue> eis::median(const std::vector<eis::DataPoint>& data)
{
if(data.empty())
return std::complex<fvalue>(0,0);
else if(data.size() == 1)
return data[0].im;
std::vector<fvalue> imagParts;
imagParts.reserve(data.size());
std::vector<fvalue> realParts;
realParts.reserve(data.size());
for(const eis::DataPoint& point : data)
{
imagParts.push_back(point.im.imag());
realParts.push_back(point.im.real());
}
fvalue realMean = medianTrampoline(realParts);
fvalue imagMean = medianTrampoline(imagParts);
return std::complex<fvalue>(realMean, imagMean);
}
fvalue eis::mean(const std::vector<fvalue>& data)
{
fvalue accum = 0;
for(fvalue point : data)
accum += point;
return accum/data.size();
}
fvalue eis::median(std::vector<fvalue> data)
{
if(data.empty())
return 0;
else if(data.size() == 1)
return data[0];
std::sort(data.begin(), data.end());
if(data.size() % 2 == 0)
return (data[data.size()/2] + data[data.size()/2-1])/2;
else
return data[data.size()/2];
}
std::vector<eis::DataPoint> eis::rescale(const std::vector<eis::DataPoint>& data, size_t outputSize)
{
std::vector<eis::DataPoint> output(outputSize);
for(size_t i = 0; i < output.size(); ++i)
{
fvalue position = static_cast<double>(i) / (output.size()-1);
fvalue sourcePosF = (data.size()-1)*position;
size_t sourcePos = (data.size()-1)*position;
fvalue frac = sourcePosF - sourcePos;
if(sourcePos >= data.size()-1)
{
output[i].im = data[data.size()-1].im;
output[i].omega = data[data.size()-1].omega;
}
else
{
output[i].im = data[sourcePos].im*(1-frac) + data[sourcePos+1].im*frac;
output[i].omega = data[sourcePos].omega*(1-frac) + data[sourcePos+1].omega*frac;
}
}
return output;
}
void eis::noise(std::vector<eis::DataPoint>& data, double amplitude, bool relative)
{
std::random_device rd;
std::default_random_engine rande(rd());
for(eis::DataPoint& point : data)
{
double realNoise = (static_cast<double>(rande() >> 1) / (rande.max() >> 1))*amplitude;
point.im.real(relative ? point.im.real()+realNoise/point.im.real() : point.im.real()+realNoise);
double imgNoise = (static_cast<double>(rande() >> 1) / (rande.max() >> 1))*amplitude;
point.im.imag(relative ? point.im.imag()+imgNoise/point.im.imag() : point.im.imag()+imgNoise);
}
}
fvalue eis::pearsonCorrelation(const std::vector<eis::DataPoint>& data)
{
std::complex<fvalue> meanValue = mean(data);
fvalue sumDeltaReDeltaIm = 0;
fvalue sumDeltaReSq = 0;
fvalue sumDeltaImSq = 0;
for(const eis::DataPoint& point : data)
{
sumDeltaReDeltaIm += (point.im.real()-meanValue.real())*(point.im.imag()-meanValue.imag());
sumDeltaReSq += pow(point.im.real()-meanValue.real(), 2);
sumDeltaImSq += pow(point.im.imag()-meanValue.imag(), 2);
}
return sumDeltaReDeltaIm/(sqrt(sumDeltaReSq)*sqrt(sumDeltaImSq));
}
fvalue eis::nonConstantScore(const std::vector<eis::DataPoint>& data)
{
std::complex<fvalue> meanValue = mean(data);
fvalue reDeviationMax = std::numeric_limits<fvalue>::min();
fvalue imDeviationMax = std::numeric_limits<fvalue>::min();
for(const eis::DataPoint& point : data)
{
fvalue reDeviation = 1-(std::abs(point.im.real())/std::abs(meanValue.real()));
fvalue imDeviation = 1-(std::abs(point.im.imag())/std::abs(meanValue.imag()));
if(reDeviationMax < reDeviation)
reDeviationMax = reDeviation;
if(imDeviationMax < imDeviation)
imDeviationMax = imDeviation;
}
return std::min(reDeviationMax, imDeviationMax);
}
fvalue eis::nyquistAreaVariance(const std::vector<eis::DataPoint>& data, eis::DataPoint* centroid)
{
assert(data.size() > 2);
eis::DataPoint localCentroid(std::complex<fvalue>(0,0));
if(!centroid)
{
centroid = &localCentroid;
for(const eis::DataPoint& point : data)
*centroid = *centroid + point;
*centroid = (*centroid)/data.size();
}
double realVar = 0;
double imagVar = 0;
double realImagVar = 0;
for(const eis::DataPoint& point : data)
{
eis::DataPoint a = point-*centroid;
realVar += std::pow(a.im.real(), 2);
imagVar += std::pow(a.im.imag(), 2);
realImagVar += a.im.real()*a.im.imag();
}
realVar /= data.size();
imagVar /= data.size();
realImagVar /= data.size();
return std::sqrt(realVar+imagVar+std::pow(realImagVar, 2));
}
fvalue eis::maximumNyquistJump(const std::vector<eis::DataPoint>& data)
{
assert(data.size() > 1);
fvalue maxDist = std::numeric_limits<fvalue>::min();
for(size_t i = 1; i < data.size(); ++i)
{
eis::DataPoint a = data[i]-data[i-1];
fvalue realVar = std::pow(a.im.real(), 2);
fvalue imagVar = std::pow(a.im.imag(), 2);
fvalue dist = std::sqrt(realVar+imagVar);
maxDist = maxDist < dist ? dist : maxDist;
}
return maxDist;
}
void eis::removeDuplicates(std::vector<eis::DataPoint>& data)
{
std::sort(data.begin(), data.end());
std::vector<eis::DataPoint>::iterator it = data.begin();
while((it = std::adjacent_find(data.begin(), data.end(),
[](const eis::DataPoint& a, const eis::DataPoint& b){return fvalueEq(a.omega, b.omega);})) != data.end())
{
data.erase(it);
}
}
bool eis::fvalueEq(fvalue a, fvalue b, unsigned int ulp)
{
fvalue epsilon = std::numeric_limits<fvalue>::epsilon()*std::fabs(a+b)*ulp;
return a - epsilon <= b && a + epsilon >= b;
}
static std::pair<std::vector<eis::DataPoint>::const_iterator, std::vector<eis::DataPoint>::const_iterator>
getLrClosest(const eis::DataPoint& dp, std::vector<eis::DataPoint>::const_iterator start, std::vector<eis::DataPoint>::const_iterator end)
{
std::vector<eis::DataPoint>::const_iterator left = end;
fvalue distLeft = std::numeric_limits<fvalue>::max();
std::vector<eis::DataPoint>::const_iterator right = end;
fvalue distRight = std::numeric_limits<fvalue>::max();
for(std::vector<eis::DataPoint>::const_iterator it = start; it != end; it++)
{
if(eis::fvalueEq(it->omega, dp.omega))
return {it, it};
fvalue dist = it->omega-dp.omega;
bool sign = std::signbit(dist);
dist = std::abs(dist);
if(sign && (left == end || dist < distLeft))
{
distLeft = dist;
left = it;
}
else if(!sign && (right == end || dist < distRight))
{
distRight = dist;
right = it;
}
}
return {left, right};
}
static bool omegaCompeare(std::pair<fvalue, std::vector<eis::DataPoint>::const_iterator> a,
std::pair<fvalue, std::vector<eis::DataPoint>::const_iterator> b)
{
return a.first < b.first;
}
static std::vector<std::pair<fvalue, std::vector<eis::DataPoint>::const_iterator>>
getSortedOmegaDistances(fvalue omega,
std::vector<eis::DataPoint>::const_iterator start,
std::vector<eis::DataPoint>::const_iterator end)
{
std::vector<std::pair<fvalue, std::vector<eis::DataPoint>::const_iterator>> out;
for(std::vector<eis::DataPoint>::const_iterator it = start; it != end; it++)
out.push_back({std::abs(it->omega-omega), it});
std::sort(out.begin(), out.end(), omegaCompeare);
return out;
}
static eis::DataPoint linearInterpolatePoint(fvalue omega, const eis::DataPoint& left, const eis::DataPoint& right)
{
assert(left.omega <= omega);
assert(right.omega >= omega);
fvalue omegaDif = right.omega - left.omega;
std::complex<fvalue> sloap = (right.im - left.im)/omegaDif;
return eis::DataPoint(left.im+sloap*(omega - left.omega), omega);
}
static eis::DataPoint linearExtrapoloatePoint(fvalue omega, const std::vector<eis::DataPoint>& data)
{
if(data.size() < 3)
throw std::invalid_argument("extrapolation requires at least 3 points");
std::vector<std::pair<fvalue, std::vector<eis::DataPoint>::const_iterator>> dist;
dist = getSortedOmegaDistances(omega, data.begin(), data.end());
std::vector<fvalue> omegas;
std::vector<fvalue> im;
std::vector<fvalue> re;
omegas.reserve(dist.size());
im.reserve(dist.size());
re.reserve(dist.size());
for(size_t i = 0; i < std::min(static_cast<size_t>(6), data.size()); ++i)
{
omegas.push_back(log10(dist[i].second->omega));
im.push_back(dist[i].second->im.imag());
re.push_back(dist[i].second->im.real());
}
LinearRegessionCalculator realReg(omegas, re);
LinearRegessionCalculator imagReg(omegas, im);
eis::Log(eis::Log::DEBUG)<<"Real regression for "<<omega<<":\n\toffset: "<<realReg.offset
<<"\n\tsloap: "<<realReg.slope<<"\n\tstderror: "<<realReg.stdError;
eis::Log(eis::Log::DEBUG)<<"Imag regression for "<<omega<<":\n\toffset: "<<imagReg.offset
<<"\n\tsloap: "<<imagReg.slope<<"\n\tstderror: "<<imagReg.stdError;
if(realReg.stdError > 3 || imagReg.stdError > 3)
throw std::invalid_argument("input data must be sufficiently linear");
std::complex<fvalue> expIm(realReg.slope*log10(omega)+realReg.offset, imagReg.slope*log10(omega)+imagReg.offset);
return eis::DataPoint(expIm, omega);
}
std::vector<eis::DataPoint> eis::fitToFrequencies(std::vector<fvalue> omegas, const std::vector<eis::DataPoint>& data, bool linearExtrapolation)
{
std::vector<eis::DataPoint> out;
out.reserve(omegas.size());
for(fvalue omega : omegas)
out.push_back(eis::DataPoint({0,0}, omega));
eis::Log(eis::Log::DEBUG)<<__func__<<':';
for(eis::DataPoint& dp : out)
{
auto lr = getLrClosest(dp, data.begin(), data.end());
eis::Log(eis::Log::DEBUG)<<"\tValue for "<<dp.omega;
if(lr.first != data.end())
eis::Log(eis::Log::DEBUG)<<"\tLeft "<<lr.first->omega<<','<<lr.first->im;
if(lr.second != data.end())
eis::Log(eis::Log::DEBUG)<<"\tRight "<<lr.second->omega<<','<<lr.second->im;
if(lr.first == lr.second)
{
dp.im = lr.first->im;
}
else if(lr.first != data.end() && lr.second != data.end())
{
dp = linearInterpolatePoint(dp.omega, *lr.first, *lr.second);
}
else if(lr.first != data.end() && lr.second == data.end())
{
try
{
if(!linearExtrapolation)
dp.im = lr.first->im;
else
dp = linearExtrapoloatePoint(dp.omega, data);
}
catch (const std::invalid_argument& ex)
{
dp.im = lr.first->im;
}
}
else if(lr.first == data.end() && lr.second != data.end())
{
try
{
if(!linearExtrapolation)
dp.im = lr.second->im;
else
dp = linearExtrapoloatePoint(dp.omega, data);
}
catch (const std::invalid_argument& ex)
{
dp.im = lr.second->im;
}
}
else
{
assert(false);
}
}
return out;
}
void eis::difference(std::vector<eis::DataPoint>& a, const std::vector<eis::DataPoint>& b)
{
assert(a.size() == b.size());
for(size_t i = 0; i < a.size(); ++i)
a[i] = a[i] - b[i];
}
//Compute simmuliarity on a bode plot
fvalue eis::eisDistance(const std::vector<eis::DataPoint>& a, const std::vector<eis::DataPoint>& b)
{
assert(a.size() == b.size());
double accum = 0;
for(size_t i = 0; i < a.size(); ++i)
{
double diffRe = std::pow(b[i].im.real() - a[i].im.real(), 2);
double diffIm = std::pow(b[i].im.imag() - a[i].im.imag(), 2);
accum += diffRe+diffIm;
}
return sqrt(accum/a.size());
}
//Compute simmuliarity on a nyquist plot
fvalue eis::eisNyquistDistance(const std::vector<eis::DataPoint>& a, const std::vector<eis::DataPoint>& b)
{
assert(a.size() > 2 && b.size() > 3);
double accum = 0;
for(size_t i = 0; i < a.size(); ++i)
{
std::vector<std::pair<double, const eis::DataPoint*>> distances;
for(size_t j = 0; j < b.size(); ++j)
{
double diffRe = std::pow(b[j].im.real() - a[i].im.real(), 2);
double diffIm = std::pow(b[j].im.imag() - a[i].im.imag(), 2);
std::pair<double, const eis::DataPoint*> dp;
dp.first = sqrt(diffRe+diffIm);
dp.second = &b[j];
distances.push_back(dp);
}
std::sort(distances.begin(), distances.end(),
[](const std::pair<double, const eis::DataPoint*>& a, const std::pair<double, const eis::DataPoint*>& b) -> bool
{return a.first < b.first;});
eis::DataPoint base = (*distances[0].second)-(*distances[1].second);
base = base/base.complexVectorLength();
eis::DataPoint diff = (*distances[0].second)-a[i];
diff = diff/diff.complexVectorLength();
fvalue dprod = base.im.real()*diff.im.real() + base.im.imag()*diff.im.imag();
fvalue dist = diff.complexVectorLength()*(1-dprod);
accum += std::pow(dist, 2);
}
return std::sqrt(accum/a.size());
}