kicad/pcb_calculator/resistor_substitution_utils...

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();
}