480 lines
15 KiB
C++
480 lines
15 KiB
C++
/*
|
|
* This program source code file
|
|
* is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2021-2023 KiCad Developers, see AUTHORS.txt for contributors.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation, either version 3 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program 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
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "resistor_substitution_utils.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <functional>
|
|
#include <stdexcept>
|
|
|
|
// If BENCHMARK is defined, calculations will print their execution time to the STDERR
|
|
// #define BENCHMARK
|
|
|
|
#ifdef BENCHMARK
|
|
#include <core/profile.h>
|
|
#endif
|
|
|
|
// Comparison operators used by std::sort and std::lower_bound
|
|
bool operator<( const RESISTANCE& aLhs, double aRhs )
|
|
{
|
|
return aLhs.value < aRhs;
|
|
}
|
|
|
|
bool operator<( const RESISTANCE& aLhs, const RESISTANCE& aRhs )
|
|
{
|
|
return aLhs.value < aRhs.value;
|
|
}
|
|
|
|
class SolutionCollector
|
|
/**
|
|
* Helper class that collects solutions and keeps one with the best deviation.
|
|
* In order to avoid performing costly string operations too frequently,
|
|
* they are postponed until the very end, when we know the best combination.
|
|
*/
|
|
{
|
|
public:
|
|
SolutionCollector( double aTarget ) : m_target( aTarget ) {}
|
|
|
|
/**
|
|
* Add two solutions, based on single 2R buffer lookup, to the collector.
|
|
*
|
|
* @param aResults are the resistances found in 2R buffer
|
|
* @param aValueFunc transforms value from aResults into final value of the combination
|
|
* @param aResultFunc transforms RESISTANCE instance from aResults into final instance
|
|
*/
|
|
void Add2RLookupResults( std::pair<RESISTANCE&, RESISTANCE&> aResults,
|
|
std::function<double( double )> aValueFunc,
|
|
std::function<RESISTANCE( RESISTANCE& )> aResultFunc )
|
|
{
|
|
addSolution( aValueFunc( aResults.first.value ), &aResults.first, aResultFunc );
|
|
addSolution( aValueFunc( aResults.second.value ), &aResults.second, aResultFunc );
|
|
}
|
|
|
|
/**
|
|
* Return the best collected combination, running the corresponding result_func.
|
|
*/
|
|
RESISTANCE GetBest()
|
|
{
|
|
if( !m_best_found_resistance )
|
|
throw std::logic_error( "Empty solution collector" );
|
|
|
|
return m_best_result_func( *m_best_found_resistance );
|
|
}
|
|
|
|
private:
|
|
/**
|
|
* Add single solution to the collector.
|
|
*
|
|
* @param aValue is a value of the combination in Ohms
|
|
* @param aFound is the corresponding RESISTANCE found in 2R buffer
|
|
* @param aResultFunc is a function calculating final result (RESISTANCE instance)
|
|
* for this combination
|
|
*/
|
|
void addSolution( double aValue, RESISTANCE* aFound,
|
|
std::function<RESISTANCE( RESISTANCE& )>& aResultFunc )
|
|
{
|
|
double deviation = std::abs( aValue - m_target );
|
|
if( deviation < m_best_deviation )
|
|
{
|
|
m_best_deviation = deviation;
|
|
m_best_found_resistance = aFound;
|
|
m_best_result_func = aResultFunc;
|
|
}
|
|
}
|
|
|
|
double m_target;
|
|
double m_best_deviation = INFINITY;
|
|
RESISTANCE* m_best_found_resistance = nullptr;
|
|
std::function<RESISTANCE( RESISTANCE& )> m_best_result_func;
|
|
};
|
|
|
|
/**
|
|
* If aText contains aRequiredSymbol as top-level (i.e. not in parentheses) operator,
|
|
* return aText enclosed in parentheses.
|
|
* Otherwise, return aText unmodified.
|
|
*/
|
|
static std::string maybeEmbrace( const std::string& aText, char aRequiredSymbol )
|
|
{
|
|
bool shouldEmbrace = false;
|
|
|
|
// scan for required top-level symbol
|
|
int parenLevel = 0;
|
|
|
|
for( char c : aText )
|
|
{
|
|
if( c == '(' )
|
|
parenLevel++;
|
|
else if( c == ')' )
|
|
parenLevel--;
|
|
else if( c == aRequiredSymbol && parenLevel == 0 )
|
|
shouldEmbrace = true;
|
|
}
|
|
|
|
// embrace or not
|
|
if( shouldEmbrace )
|
|
return '(' + aText + ')';
|
|
else
|
|
return aText;
|
|
}
|
|
|
|
/**
|
|
* Functions calculating values and text representations of serial and parallel combinations.
|
|
* Functions marked as 'Simple' do not care about parentheses, which makes them faster.
|
|
*/
|
|
|
|
static inline double serialValue( double aR1, double aR2 )
|
|
{
|
|
return aR1 + aR2;
|
|
}
|
|
|
|
static inline double parallelValue( double aR1, double aR2 )
|
|
{
|
|
return aR1 * aR2 / ( aR1 + aR2 );
|
|
}
|
|
|
|
static inline RESISTANCE serialResistance( const RESISTANCE& aR1, const RESISTANCE& aR2 )
|
|
{
|
|
std::string name = maybeEmbrace( aR1.name, '|' ) + " + " + maybeEmbrace( aR2.name, '|' );
|
|
return RESISTANCE( serialValue( aR1.value, aR2.value ), name );
|
|
}
|
|
|
|
static inline RESISTANCE parallelResistance( const RESISTANCE& aR1, const RESISTANCE& aR2 )
|
|
{
|
|
std::string name = maybeEmbrace( aR1.name, '+' ) + " | " + maybeEmbrace( aR2.name, '+' );
|
|
return RESISTANCE( parallelValue( aR1.value, aR2.value ), name );
|
|
}
|
|
|
|
static inline RESISTANCE serialResistanceSimple( const RESISTANCE& aR1, const RESISTANCE& aR2 )
|
|
{
|
|
std::string name = aR1.name + " + " + aR2.name;
|
|
return RESISTANCE( serialValue( aR1.value, aR2.value ), name );
|
|
}
|
|
|
|
static inline RESISTANCE parallelResistanceSimple( const RESISTANCE& aR1, const RESISTANCE& aR2 )
|
|
{
|
|
std::string name = aR1.name + " | " + aR2.name;
|
|
return RESISTANCE( parallelValue( aR1.value, aR2.value ), name );
|
|
}
|
|
|
|
// Return a string from aValue (aValue is expected in ohms).
|
|
// If aValue < 1000 the returned string is aValue with unit = R.
|
|
// If aValue >= 1000 the returned string is aValue/1000 with unit = K
|
|
// with notation similar to 2K2.
|
|
// If aValue >= 1e6 the returned string is aValue/1e6 with unit = M
|
|
// with notation = 1M.
|
|
static std::string strValue( double aValue )
|
|
{
|
|
std::string result;
|
|
|
|
if( aValue < 1000.0 )
|
|
{
|
|
result = std::to_string( static_cast<int>( aValue ) );
|
|
result += 'R';
|
|
}
|
|
else
|
|
{
|
|
double div = 1e3;
|
|
char unit = 'K';
|
|
|
|
if( aValue >= 1e6 )
|
|
{
|
|
div = 1e6;
|
|
unit = 'M';
|
|
}
|
|
|
|
aValue /= div;
|
|
|
|
int valueAsInt = static_cast<int>( aValue );
|
|
result = std::to_string( valueAsInt );
|
|
result += unit;
|
|
|
|
// Add mantissa: 1 digit, suitable for series up to E24
|
|
double mantissa = aValue - valueAsInt;
|
|
|
|
if( mantissa > 0 )
|
|
result += std::to_string( lround( mantissa * 10 ) );
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
RES_EQUIV_CALC::RES_EQUIV_CALC()
|
|
{
|
|
// series must be added to vector in correct order
|
|
m_e_series.push_back( buildSeriesData( ESERIES::E1_VALUES() ) );
|
|
m_e_series.push_back( buildSeriesData( ESERIES::E3_VALUES() ) );
|
|
m_e_series.push_back( buildSeriesData( ESERIES::E6_VALUES() ) );
|
|
m_e_series.push_back( buildSeriesData( ESERIES::E12_VALUES() ) );
|
|
m_e_series.push_back( buildSeriesData( ESERIES::E24_VALUES() ) );
|
|
}
|
|
|
|
void RES_EQUIV_CALC::SetSeries( uint32_t aSeries )
|
|
{
|
|
m_series = aSeries;
|
|
}
|
|
|
|
void RES_EQUIV_CALC::NewCalc( double aTargetValue )
|
|
{
|
|
m_target = aTargetValue;
|
|
|
|
m_exclude_mask.resize( m_e_series[m_series].size() );
|
|
std::fill( m_exclude_mask.begin(), m_exclude_mask.end(), false );
|
|
|
|
std::fill( m_results.begin(), m_results.end(), std::nullopt );
|
|
}
|
|
|
|
void RES_EQUIV_CALC::Exclude( double aValue )
|
|
{
|
|
if( std::isnan( aValue ) )
|
|
return;
|
|
|
|
std::vector<RESISTANCE>& series = m_e_series[m_series];
|
|
auto it = std::lower_bound( series.begin(), series.end(), aValue - epsilon );
|
|
|
|
if( it != series.end() && std::abs( it->value - aValue ) < epsilon )
|
|
m_exclude_mask[it - series.begin()] = true;
|
|
}
|
|
|
|
void RES_EQUIV_CALC::Calculate()
|
|
{
|
|
#ifdef BENCHMARK
|
|
PROF_TIMER timer( "Resistor calculation" );
|
|
#endif
|
|
|
|
prepare1RBuffer();
|
|
prepare2RBuffer();
|
|
|
|
RESISTANCE solution_2r = calculate2RSolution();
|
|
m_results[S2R] = solution_2r;
|
|
|
|
if( std::abs( solution_2r.value - m_target ) > epsilon )
|
|
{
|
|
RESISTANCE solution_3r = calculate3RSolution();
|
|
m_results[S3R] = solution_3r;
|
|
|
|
if( std::abs( solution_3r.value - m_target ) > epsilon )
|
|
m_results[S4R] = calculate4RSolution();
|
|
}
|
|
|
|
#ifdef BENCHMARK
|
|
timer.Show();
|
|
#endif
|
|
}
|
|
|
|
std::vector<RESISTANCE> RES_EQUIV_CALC::buildSeriesData( const ESERIES::ESERIES_VALUES& aList )
|
|
{
|
|
std::vector<RESISTANCE> result_list;
|
|
|
|
for( double curr_decade = RES_EQUIV_CALC_FIRST_VALUE;; curr_decade *= 10.0 ) // iterate over decades
|
|
{
|
|
double multiplier = curr_decade / aList[0];
|
|
|
|
for( const uint16_t listvalue : aList ) // iterate over values in decade
|
|
{
|
|
double value = multiplier * listvalue;
|
|
result_list.emplace_back( value, strValue( value ) );
|
|
|
|
if( value >= RES_EQUIV_CALC_LAST_VALUE )
|
|
return result_list;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RES_EQUIV_CALC::prepare1RBuffer()
|
|
{
|
|
std::vector<RESISTANCE>& series = m_e_series[m_series];
|
|
m_buffer_1R.clear();
|
|
|
|
for( size_t i = 0; i < series.size(); i++ )
|
|
{
|
|
if( !m_exclude_mask[i] )
|
|
m_buffer_1R.push_back( series[i] );
|
|
}
|
|
}
|
|
|
|
void RES_EQUIV_CALC::prepare2RBuffer()
|
|
{
|
|
m_buffer_2R.clear();
|
|
|
|
for( size_t i1 = 0; i1 < m_buffer_1R.size(); i1++ )
|
|
{
|
|
for( size_t i2 = i1; i2 < m_buffer_1R.size(); i2++ )
|
|
{
|
|
m_buffer_2R.push_back( serialResistanceSimple( m_buffer_1R[i1], m_buffer_1R[i2] ) );
|
|
m_buffer_2R.push_back( parallelResistanceSimple( m_buffer_1R[i1], m_buffer_1R[i2] ) );
|
|
}
|
|
}
|
|
|
|
std::sort( m_buffer_2R.begin(), m_buffer_2R.end() );
|
|
}
|
|
|
|
std::pair<RESISTANCE&, RESISTANCE&> RES_EQUIV_CALC::findIn2RBuffer( double aTarget )
|
|
{
|
|
// in case of NaN, return anything valid
|
|
if( std::isnan( aTarget ) )
|
|
return { m_buffer_2R[0], m_buffer_2R[0] };
|
|
|
|
// target value is often too small or too big, so check that manually
|
|
if( aTarget <= m_buffer_2R.front().value || aTarget >= m_buffer_2R.back().value )
|
|
return { m_buffer_2R.front(), m_buffer_2R.back() };
|
|
|
|
auto it = std::lower_bound( m_buffer_2R.begin(), m_buffer_2R.end(), aTarget )
|
|
- m_buffer_2R.begin();
|
|
|
|
if( it == 0 )
|
|
return { m_buffer_2R[0], m_buffer_2R[0] };
|
|
else if( it == m_buffer_2R.size() )
|
|
return { m_buffer_2R[it - 1], m_buffer_2R[it - 1] };
|
|
else
|
|
return { m_buffer_2R[it - 1], m_buffer_2R[it] };
|
|
}
|
|
|
|
RESISTANCE RES_EQUIV_CALC::calculate2RSolution()
|
|
{
|
|
SolutionCollector solution( m_target );
|
|
|
|
auto valueFunc = []( double aFoundValue )
|
|
{
|
|
return aFoundValue;
|
|
};
|
|
auto resultFunc = []( RESISTANCE& aFoundRes )
|
|
{
|
|
return aFoundRes;
|
|
};
|
|
solution.Add2RLookupResults( findIn2RBuffer( m_target ), valueFunc, resultFunc );
|
|
|
|
return solution.GetBest();
|
|
}
|
|
|
|
RESISTANCE RES_EQUIV_CALC::calculate3RSolution()
|
|
{
|
|
SolutionCollector solution( m_target );
|
|
|
|
for( RESISTANCE& r : m_buffer_1R )
|
|
{
|
|
// try r + 2R combination
|
|
{
|
|
auto valueFunc = [&]( double aFoundValue )
|
|
{
|
|
return serialValue( aFoundValue, r.value );
|
|
};
|
|
auto resultFunc = [&]( RESISTANCE& aFoundRes )
|
|
{
|
|
return serialResistance( aFoundRes, r );
|
|
};
|
|
solution.Add2RLookupResults( findIn2RBuffer( m_target - r.value ), valueFunc,
|
|
resultFunc );
|
|
}
|
|
|
|
// try r | 2R combination
|
|
{
|
|
auto valueFunc = [&]( double aFoundValue )
|
|
{
|
|
return parallelValue( aFoundValue, r.value );
|
|
};
|
|
auto resultFunc = [&]( RESISTANCE& aFoundRes )
|
|
{
|
|
return parallelResistance( aFoundRes, r );
|
|
};
|
|
solution.Add2RLookupResults(
|
|
findIn2RBuffer( m_target * r.value / ( r.value - m_target ) ), valueFunc,
|
|
resultFunc );
|
|
}
|
|
}
|
|
|
|
return solution.GetBest();
|
|
}
|
|
|
|
RESISTANCE RES_EQUIV_CALC::calculate4RSolution()
|
|
{
|
|
SolutionCollector solution( m_target );
|
|
|
|
for( RESISTANCE& rr : m_buffer_2R )
|
|
{
|
|
// try 2R + 2R combination
|
|
{
|
|
auto valueFunc = [&]( double aFoundValue )
|
|
{
|
|
return serialValue( aFoundValue, rr.value );
|
|
};
|
|
auto resultFunc = [&]( RESISTANCE& aFoundRes )
|
|
{
|
|
return serialResistance( aFoundRes, rr );
|
|
};
|
|
solution.Add2RLookupResults( findIn2RBuffer( m_target - rr.value ), valueFunc,
|
|
resultFunc );
|
|
}
|
|
|
|
// try 2R | 2R combination
|
|
{
|
|
auto valueFunc = [&]( double aFoundValue )
|
|
{
|
|
return parallelValue( aFoundValue, rr.value );
|
|
};
|
|
auto resultFunc = [&]( RESISTANCE& aFoundRes )
|
|
{
|
|
return parallelResistance( aFoundRes, rr );
|
|
};
|
|
solution.Add2RLookupResults(
|
|
findIn2RBuffer( m_target * rr.value / ( rr.value - m_target ) ), valueFunc,
|
|
resultFunc );
|
|
}
|
|
}
|
|
|
|
for( RESISTANCE& r1 : m_buffer_1R )
|
|
{
|
|
for( RESISTANCE& r2 : m_buffer_1R )
|
|
{
|
|
// try r1 + (r2 | 2R)
|
|
{
|
|
auto valueFunc = [&]( double aFoundValue )
|
|
{
|
|
return serialValue( r1.value, parallelValue( r2.value, aFoundValue ) );
|
|
};
|
|
auto resultFunc = [&]( RESISTANCE& aFoundRes )
|
|
{
|
|
return serialResistance( r1, parallelResistance( r2, aFoundRes ) );
|
|
};
|
|
solution.Add2RLookupResults( findIn2RBuffer( ( m_target - r1.value ) * r2.value
|
|
/ ( r1.value + r2.value - m_target ) ),
|
|
valueFunc, resultFunc );
|
|
}
|
|
|
|
// try r1 | (r2 + 2R)
|
|
{
|
|
auto valueFunc = [&]( double aFoundValue )
|
|
{
|
|
return parallelValue( r1.value, serialValue( r2.value, aFoundValue ) );
|
|
};
|
|
auto resultFunc = [&]( RESISTANCE& aFoundRes )
|
|
{
|
|
return parallelResistance( r1, serialResistance( r2, aFoundRes ) );
|
|
};
|
|
solution.Add2RLookupResults(
|
|
findIn2RBuffer( m_target * r1.value / ( r1.value - m_target ) - r2.value ),
|
|
valueFunc, resultFunc );
|
|
}
|
|
}
|
|
}
|
|
|
|
return solution.GetBest();
|
|
}
|