Implement new resistor substitution algorithm

Significantly improves performance of the tool.  Also corrects an mistake in the formula for computing relative errors
This commit is contained in:
Bartek Wacławik 2023-09-22 21:43:09 +00:00 committed by Seth Hillbrand
parent 463e7c3b30
commit ac9a863496
3 changed files with 501 additions and 476 deletions

View File

@ -35,6 +35,13 @@
wxString r_calculator_help =
#include "r_calculator_help.h"
/* RES_EQUIV_CALC class considers resistor values from a limited range
* and only combinations of up to 4 resistors, so target values less than
* parallel combination of minimum resistors or greater than serial combination
* of maximum resistors cannot be reasonably looked for
*/
static const double min_target_value = static_cast<double>(RES_EQUIV_CALC_FIRST_VALUE) / 4;
static const double max_target_value = static_cast<double>(RES_EQUIV_CALC_LAST_VALUE) * 4;
extern double DoubleFromString( const wxString& TextValue );
@ -94,22 +101,25 @@ void PANEL_R_CALCULATOR::LoadSettings( PCB_CALCULATOR_SETTINGS* aCfg )
void PANEL_R_CALCULATOR::OnCalculateESeries( wxCommandEvent& event )
{
double reqr; // required resistor stored in local copy
double error, err3 = 0;
wxString es, fs; // error and formula strings
double reqr = 1000 * DoubleFromString( m_ResRequired->GetValue() );
wxBusyCursor dummy;
if( std::isnan( reqr ) || reqr < min_target_value || reqr > max_target_value )
{
wxMessageBox( wxString::Format( _( "Incorrect required resistance value: %s" ),
m_ResRequired->GetValue() ) );
return;
}
wxBusyCursor busyCursor; // As long as this variable exists, the cursor will be 'busy'
reqr = ( 1000 * DoubleFromString( m_ResRequired->GetValue() ) );
m_eSeries.SetRequiredValue( reqr ); // keep a local copy of required resistor value
m_eSeries.NewCalc(); // assume all values available
m_eSeries.NewCalc( reqr ); // assume all values available
/*
* Exclude itself. For the case, a value from the available series is found as required value,
* the calculator assumes this value needs a replacement for the reason of being not available.
* Two further exclude values can be entered to exclude and are skipped as not being available.
* All values entered in KiloOhms are converted to Ohm for internal calculation
*/
m_eSeries.Exclude( 1000 * DoubleFromString( m_ResRequired->GetValue() ) );
m_eSeries.Exclude( reqr );
m_eSeries.Exclude( 1000 * DoubleFromString( m_ResExclude1->GetValue() ) );
m_eSeries.Exclude( 1000 * DoubleFromString( m_ResExclude2->GetValue() ) );
@ -117,88 +127,43 @@ void PANEL_R_CALCULATOR::OnCalculateESeries( wxCommandEvent& event )
{
m_eSeries.Calculate();
}
catch( std::out_of_range const& exc )
catch( const std::exception& exc )
{
wxString msg;
msg << "Internal error: " << exc.what();
wxMessageBox( msg );
wxMessageBox( wxString::Format( "Internal error: %s", exc.what() ) );
return;
}
fs = m_eSeries.GetResults()[RES_EQUIV_CALC::S2R].e_name; // show 2R solution formula string
m_ESeries_Sol2R->SetValue( fs );
error = reqr
+ m_eSeries.GetResults()[RES_EQUIV_CALC::S2R].e_value; // absolute value of solution
error = ( reqr / error - 1 ) * 100; // error in percent
if( error )
auto showResult = [reqr]( const std::optional<RESISTANCE>& aResult, wxTextCtrl* aFormulaField,
wxTextCtrl* aErrorField )
{
if( std::abs( error ) < 0.01 )
es.Printf( "<%.2f", 0.01 );
else
es.Printf( "%+.2f", error );
}
else
{
es = _( "Exact" );
}
wxString fs, es; // formula and error string
m_ESeriesError2R->SetValue( es ); // anyway show 2R error string
if( m_eSeries.GetResults()[RES_EQUIV_CALC::S3R].e_use ) // if 3R solution available
{
err3 = reqr + m_eSeries.GetResults()[RES_EQUIV_CALC::S3R].e_value; // calculate the 3R
err3 = ( reqr / err3 - 1 ) * 100; // error in percent
if( err3 )
if( aResult ) // if value is present
{
if( std::abs( err3 ) < 0.01 )
fs = aResult->name;
double sol = aResult->value;
double error = ( sol / reqr - 1 ) * 100; // relative error in percent
if( std::abs( error ) < epsilon )
es = _( "Exact" );
else if( std::abs( error ) < 0.01 )
es.Printf( "<%.2f", 0.01 );
else
es.Printf( "%+.2f", err3 );
es.Printf( "%+.2f", error );
}
else
{
es = _( "Exact" );
fs = _( "Not worth using" );
es = wxEmptyString;
}
m_ESeriesError3R->SetValue( es ); // show 3R error string
fs = m_eSeries.GetResults()[RES_EQUIV_CALC::S3R].e_name;
m_ESeries_Sol3R->SetValue( fs ); // show 3R formula string
}
else // nothing better than 2R found
{
fs = _( "Not worth using" );
m_ESeries_Sol3R->SetValue( fs );
m_ESeriesError3R->SetValue( wxEmptyString );
}
aFormulaField->SetValue( fs );
aErrorField->SetValue( es );
};
fs = wxEmptyString;
if( m_eSeries.GetResults()[RES_EQUIV_CALC::S4R].e_use ) // show 4R solution if available
{
fs = m_eSeries.GetResults()[RES_EQUIV_CALC::S4R].e_name;
error = reqr
+ m_eSeries.GetResults()[RES_EQUIV_CALC::S4R].e_value; // absolute value of solution
error = ( reqr / error - 1 ) * 100; // error in percent
if( error )
es.Printf( "%+.2f", error );
else
es = _( "Exact" );
m_ESeriesError4R->SetValue( es );
}
else // no 4R solution
{
fs = _( "Not worth using" );
es = wxEmptyString;
m_ESeriesError4R->SetValue( es );
}
m_ESeries_Sol4R->SetValue( fs );
showResult( m_eSeries.GetResults()[RES_EQUIV_CALC::S2R], m_ESeries_Sol2R, m_ESeriesError2R );
showResult( m_eSeries.GetResults()[RES_EQUIV_CALC::S3R], m_ESeries_Sol3R, m_ESeriesError3R );
showResult( m_eSeries.GetResults()[RES_EQUIV_CALC::S4R], m_ESeries_Sol4R, m_ESeriesError4R );
}

View File

@ -2,7 +2,6 @@
* This program source code file
* is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2020 <janvi@veith.net>
* Copyright (C) 2021-2023 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software: you can redistribute it and/or modify it
@ -19,30 +18,167 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cstdint>
#include <string>
#include <algorithm>
#include <limits>
#include "resistor_substitution_utils.h"
#include "eseries.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <stdexcept>
/*
* If BENCHMARK is defined, any 4R E12 calculations will print its execution time to console
* My Hasswell Enthusiast reports 225 mSec what are reproducible within plusminus 2 percent
*/
//#define BENCHMARK
// 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;
}
// Return a string from aValue (aValue is expected in ohms)
// If aValue < 1000 the returned string is aValue with unit = R
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
// with notation similar to 2K2.
// If aValue >= 1e6 the returned string is aValue/1e6 with unit = M
// with notation = 1M
// with notation = 1M.
static std::string strValue( double aValue )
{
std::string result;
@ -73,7 +209,7 @@ static std::string strValue( double aValue )
double mantissa = aValue - valueAsInt;
if( mantissa > 0 )
result += std::to_string( static_cast<int>( ( mantissa * 10 ) + 0.5 ) );
result += std::to_string( lround( mantissa * 10 ) );
}
return result;
@ -82,312 +218,262 @@ static std::string strValue( double aValue )
RES_EQUIV_CALC::RES_EQUIV_CALC()
{
// Build the list of available resistor values in each En serie
const ESERIES::ESERIES_VALUES listValuesE1 = ESERIES::E1_VALUES();
const ESERIES::ESERIES_VALUES listValuesE3 = ESERIES::E3_VALUES();
const ESERIES::ESERIES_VALUES listValuesE6 = ESERIES::E6_VALUES();
const ESERIES::ESERIES_VALUES listValuesE12 = ESERIES::E12_VALUES();
const ESERIES::ESERIES_VALUES listValuesE24 = ESERIES::E24_VALUES();
// buildSeriesData must be called in the order of En series, because
// the list of series is expected indexed by En for the serie En
buildSeriesData( listValuesE1 );
buildSeriesData( listValuesE3 );
buildSeriesData( listValuesE6 );
buildSeriesData( listValuesE12 );
int count = buildSeriesData( listValuesE24 );
// Reserve a buffer for intermediate calculations:
// the buffer size is 2*count*count to store all combinations of 2 values
// there are 2*count*count = 29282 combinations for E24
int bufsize = 2 * count * count;
m_combined_table.reserve( bufsize );
// Store predefined R_DATA items.
for( int ii = 0; ii < bufsize; ii++ )
m_combined_table.emplace_back( "", 0.0 );
// 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() ) );
}
int RES_EQUIV_CALC::buildSeriesData( const ESERIES::ESERIES_VALUES aList )
void RES_EQUIV_CALC::SetSeries( uint32_t aSeries )
{
uint_fast32_t curr_decade = FIRST_VALUE;
int count = 0;
std::vector<R_DATA> curr_list;
uint_fast16_t first_value_in_decade = aList[0];
for( ;; )
{
double curr_r = LAST_VALUE;
for( const uint16_t listvalue : aList )
{
curr_r = 1.0 * curr_decade * listvalue / first_value_in_decade;
curr_list.emplace_back( strValue( curr_r ), curr_r );
count++;
if( curr_r >= LAST_VALUE )
break;
}
if( curr_r >= LAST_VALUE )
break;
curr_decade *= 10;
}
m_tables.push_back( std::move( curr_list ) );
return count;
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( aValue != 0.0 ) // if there is a value to exclude other than a wire jumper
{
for( R_DATA& i : m_tables[m_series] ) // then search it in the selected E-Series table
{
if( i.e_value == aValue ) // if the value to exclude is found
i.e_use = false; // disable its use
}
}
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::simple_solution( uint32_t aSize )
{
uint32_t i;
m_results.at( S2R ).e_value =
std::numeric_limits<double>::max(); // assume no 2R solution or max deviation
for( i = 0; i < aSize; i++ )
{
if( std::abs( m_combined_table.at( i ).e_value - m_required_value )
< std::abs( m_results.at( S2R ).e_value ) )
{
m_results[S2R].e_value =
m_combined_table[i].e_value - m_required_value; // save signed deviation in Ohms
m_results[S2R].e_name = m_combined_table[i].e_name; // save combination text
m_results[S2R].e_use = true; // this is a possible solution
}
}
}
void RES_EQUIV_CALC::combine4( uint32_t aSize )
{
uint32_t i, j;
double tmp;
m_results[S4R].e_use = false; // disable 4R solution, until
m_results[S4R].e_value = m_results[S3R].e_value; // 4R becomes better than 3R solution
#ifdef BENCHMARK
PROF_TIMER timer; // start timer to count execution time
#endif
for( i = 0; i < aSize; i++ ) // 4R search outer loop
{ // scan valid intermediate 2R solutions
for( j = 0; j < aSize; j++ ) // inner loop combines all with itself
{
tmp = m_combined_table[i].e_value
+ m_combined_table[j].e_value; // calculate 2R+2R serial
tmp -= m_required_value; // calculate 4R deviation
if( std::abs( tmp ) < std::abs( m_results.at( S4R ).e_value ) ) // if new 4R is better
{
m_results[S4R].e_value = tmp; // save amount of benefit
std::string s = "( ";
s.append( m_combined_table[i].e_name ); // mention 1st 2 component
s.append( " ) + ( " ); // in series
s.append( m_combined_table[j].e_name ); // with 2nd 2 components
s.append( " )" );
m_results[S4R].e_name = s; // save the result and
m_results[S4R].e_use = true; // enable for later use
}
tmp = ( m_combined_table[i].e_value * m_combined_table[j].e_value )
/ ( m_combined_table[i].e_value
+ m_combined_table[j].e_value ); // calculate 2R|2R parallel
tmp -= m_required_value; // calculate 4R deviation
if( std::abs( tmp ) < std::abs( m_results[S4R].e_value ) ) // if new 4R is better
{
m_results[S4R].e_value = tmp; // save amount of benefit
std::string s = "( ";
s.append( m_combined_table[i].e_name ); // mention 1st 2 component
s.append( " ) | ( " ); // in parallel
s.append( m_combined_table[j].e_name ); // with 2nd 2 components
s.append( " )" );
m_results[S4R].e_name = s; // save the result
m_results[S4R].e_use = true; // enable later use
}
}
}
#ifdef BENCHMARK
printf( "Calculation time = %d mS", timer.msecs() );
fflush( 0 );
#endif
}
void RES_EQUIV_CALC::NewCalc()
{
for( R_DATA& i : m_combined_table )
i.e_use = false; // before any calculation is done, assume that
for( R_DATA& i : m_results )
i.e_use = false; // no combinations and no results are available
for( R_DATA& i : m_tables[m_series] )
i.e_use = true; // all selected E-values available
}
uint32_t RES_EQUIV_CALC::combine2()
{
uint32_t combi2R = 0; // target index counts calculated 2R combinations
std::string s;
for( const R_DATA& i : m_tables[m_series] ) // outer loop to sweep selected source lookup table
{
if( i.e_use )
{
for( const R_DATA& j : m_tables[m_series] ) // inner loop to combine values with itself
{
if( j.e_use )
{
m_combined_table[combi2R].e_use = true;
m_combined_table[combi2R].e_value =
i.e_value + j.e_value; // calculate 2R serial
s = i.e_name;
s.append( " + " );
m_combined_table[combi2R].e_name = s.append( j.e_name );
combi2R++; // next destination
m_combined_table[combi2R].e_use = true; // calculate 2R parallel
m_combined_table[combi2R].e_value =
i.e_value * j.e_value / ( i.e_value + j.e_value );
s = i.e_name;
s.append( " | " );
m_combined_table[combi2R].e_name = s.append( j.e_name );
combi2R++; // next destination
}
}
}
}
return combi2R;
}
void RES_EQUIV_CALC::combine3( uint32_t aSize )
{
uint32_t j = 0;
double tmp = 0; // avoid warning for being uninitialized
std::string s;
m_results[S3R].e_use = false; // disable 3R solution, until 3R
m_results[S3R].e_value = m_results[S2R].e_value; // becomes better than 2R solution
for( const R_DATA& i : m_tables[m_series] ) // 3R Outer loop to selected primary E series table
{
if( i.e_use ) // skip all excluded values
{
for( j = 0; j < aSize; j++ ) // inner loop combines with all 2R intermediate
{ // results R+2R serial combi
tmp = m_combined_table[j].e_value + i.e_value;
tmp -= m_required_value; // calculate deviation
if( std::abs( tmp ) < std::abs( m_results[S3R].e_value ) ) // compare if better
{ // then take it
s = i.e_name; // mention 3rd component
s.append( " + ( " ); // in series
s.append( m_combined_table[j].e_name ); // with 2R combination
s.append( " )" );
m_results[S3R].e_name = s; // save S3R result
m_results[S3R].e_value = tmp; // save amount of benefit
m_results[S3R].e_use = true; // enable later use
}
tmp = i.e_value * m_combined_table[j].e_value
/ ( i.e_value + m_combined_table[j].e_value ); // calculate R + 2R parallel
tmp -= m_required_value; // calculate deviation
if( std::abs( tmp ) < std::abs( m_results[S3R].e_value ) ) // compare if better
{ // then take it
s = i.e_name; // mention 3rd component
s.append( " | ( " ); // in parallel
s.append( m_combined_table[j].e_name ); // with 2R combination
s.append( " )" );
m_results[S3R].e_name = s;
m_results[S3R].e_value = tmp; // save amount of benefit
m_results[S3R].e_use = true; // enable later use
}
}
}
}
// If there is a 3R result with remaining deviation consider to search a possibly better
// 4R solution
// calculate 4R for small series always
if( m_results[S3R].e_use && tmp )
combine4( aSize );
}
void RES_EQUIV_CALC::Calculate()
{
uint32_t no_of_2Rcombi = 0;
#ifdef BENCHMARK
PROF_TIMER timer( "Resistor calculation" );
#endif
no_of_2Rcombi = combine2(); // combine all 2R combinations for selected E serie
simple_solution( no_of_2Rcombi ); // search for simple 2 component solution
prepare1RBuffer();
prepare2RBuffer();
if( m_results[S2R].e_value ) // if simple 2R result is not exact
combine3( no_of_2Rcombi ); // continiue searching for a possibly better solution
RESISTANCE solution_2r = calculate2RSolution();
m_results[S2R] = solution_2r;
strip3();
strip4();
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
}
void RES_EQUIV_CALC::strip3()
std::vector<RESISTANCE> RES_EQUIV_CALC::buildSeriesData( const ESERIES::ESERIES_VALUES& aList )
{
std::string s;
std::vector<RESISTANCE> result_list;
if( m_results[S3R].e_use ) // if there is a 3 term result available
{ // what is connected either by two "|" or by 3 plus
s = m_results[S3R].e_name;
for( double curr_decade = RES_EQUIV_CALC_FIRST_VALUE;; curr_decade *= 10.0 ) // iterate over decades
{
double multiplier = curr_decade / aList[0];
if( ( std::count( s.begin(), s.end(), '+' ) == 2 )
|| ( std::count( s.begin(), s.end(), '|' ) == 2 ) )
{ // then strip one pair of braces
s.erase( s.find( "( " ), 2 ); // it is known sure, this is available
s.erase( s.find( " )" ), 2 ); // in any unstripped 3R result term
m_results[S3R].e_name = s; // use stripped result
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::strip4()
void RES_EQUIV_CALC::prepare1RBuffer()
{
std::string s;
std::vector<RESISTANCE>& series = m_e_series[m_series];
m_buffer_1R.clear();
if( m_results[S4R].e_use ) // if there is a 4 term result available
{ // what are connected either by 3 "+" or by 3 "|"
s = m_results[S4R].e_name;
if( ( std::count( s.begin(), s.end(), '+' ) == 3 )
|| ( std::count( s.begin(), s.end(), '|' ) == 3 ) )
{ // then strip two pair of braces
s.erase( s.find( "( " ), 2 ); // it is known sure, they are available
s.erase( s.find( " )" ), 2 ); // in any unstripped 4R result term
s.erase( s.find( "( " ), 2 );
s.erase( s.find( " )" ), 2 );
m_results[S4R].e_name = s; // use stripped result
}
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();
}

View File

@ -20,168 +20,142 @@
#pragma once
#include <string>
#include <cstdint>
#include <vector>
#include <array>
#include "eseries.h"
#include <array>
#include <optional>
#include <string>
#include <utility>
#include <vector>
// First value of resistor in ohm
const double epsilon = 1e-12; // machine epsilon for floating-point equality testing
// First value of resistor in Ohm
// It should be first value of the decade, i.e. power of 10
// This value is only pertinent to the resistor calculator.
// It is used to reduce the computational complexity of its calculations.
// There are valid resistor values using E-series numbers below this
// value and above the below LAST_VALUE.
#define FIRST_VALUE 10
#define RES_EQUIV_CALC_FIRST_VALUE 10
// last value of resistor in ohm
// Last value of resistor in Ohm
// This value is only pertinent to the resistor calculator. See above.
#define LAST_VALUE 1e6
#define RES_EQUIV_CALC_LAST_VALUE 1e6
// R_DATA handles a resistor: string value, value and allowed to use
struct R_DATA
// Struct representing resistance value together with its composition, e.g. {20.0, "10R + 10R"}
struct RESISTANCE
{
R_DATA() : e_use( true ), e_value( 0.0 ) {}
double value;
std::string name;
R_DATA( const std::string& aName, double aValue )
RESISTANCE( double aValue = 0.0, std::string aName = "" ) :
value( aValue ), name( std::move( aName ) )
{
e_use = true;
e_name = aName;
e_value = aValue;
}
bool e_use;
std::string e_name;
double e_value;
};
class RES_EQUIV_CALC
/*! \brief Performs calculations on E-series values primarily to find target values.
/*! \brief Performs calculations on E-series values primarily to find target values
* as combinations (serial, parallel) of them.
*
* E_SERIES class stores and performs calcuations on E-series values. It currently
* RES_EQUIV_CALC class stores and performs calcuations on E-series values. It currently
* is targeted toward the resistor calculator and hard codes some limitations
* to optimize its use in the resistor calculator.
*
* At this time these limitations are that this class ignores all E-series larger
* than E24 and it does not consider resistor values below 10 Ohm or above 1M Ohm.
* At this time these limitations are that this class handles only E-series up to
* E24 and it does not consider resistor values below 10 Ohm or above 1M Ohm.
*/
{
public:
RES_EQUIV_CALC();
/**
* This calculator suggests solutions for 2R, 3R and 4R replacement combinations
*/
enum
{
S2R,
S3R,
S4R
S4R,
NUMBER_OF_LEVELS
};
/**
* Set E-series to be used in calculations.
* Correct values are from 0 to 4 inclusive,
* representing series (consecutively) E1, E3, E6, E12, E24.
* After changing the series, NewCalc must be called before running calculations.
*/
void SetSeries( uint32_t aSeries );
/**
* Initialize next calculation, clear exclusion mask
* and erase results from previous calculation.
*
* @param aTargetValue is the value (in Ohms) to be looked for
*/
void NewCalc( double aTargetValue );
/**
* If any value of the selected E-series not available, it can be entered as an exclude value.
*
* @param aValue is the value to exclude from calculation
* Values to exclude are set to false in the selected E-series source lookup table
* @param aValue is the value (in Ohms) to exclude from calculation
* Values to exclude are set to true in the current exclusion mask and will not be
* considered during calculations.
*/
void Exclude( double aValue );
/**
* initialize next calculation and erase results from previous calculation
*/
void NewCalc();
/**
* called on calculate button to execute all the 2R, 3R and 4R calculations
* Executes all the calculations.
* Results are to be retrieved using GetResults (below).
*/
void Calculate();
/**
* Interface for CheckBox, RadioButton, RequriedResistor and calculated Results
* Accessor to calculation results.
* Empty std::optional means that the exact value can be achieved using fewer resistors.
*/
void SetSeries( uint32_t aSeries ) { m_series = aSeries; }
void SetRequiredValue( double aValue ) { m_required_value = aValue; }
// Accessor:
const std::array<R_DATA, S4R + 1>& GetResults() { return m_results; }
const std::array<std::optional<RESISTANCE>, NUMBER_OF_LEVELS>& GetResults()
{
return m_results;
}
private:
/**
* Add values from aList to m_tables. Covers all decades between FIRST_VALUE and LAST_VALUE.
* @return the count of items added to m_tables.
* Add values from aList to m_e_series tables.
* Covers all decades between FIRST_VALUE and LAST_VALUE.
*/
int buildSeriesData( const ESERIES::ESERIES_VALUES );
std::vector<RESISTANCE> buildSeriesData( const ESERIES::ESERIES_VALUES& aList );
/**
* Build all 2R combinations from the selected E-series values
*
* Pre-calculated value combinations are saved in intermediate look up table m_combined_table
* @return is the number of found combinations what also depends from exclude values
*/
uint32_t combine2();
* Build 1R buffer, which is selected E-series table with excluded values removed.
*/
void prepare1RBuffer();
/**
* Search for closest two component solution
*
* @param aSize is the number of valid 2R combinations in m_combined_table on where to search
* The 2R result with smallest deviation will be saved in results
*/
void simple_solution( uint32_t aSize );
* Build 2R buffer, which consists of all possible combinations of two resistors
* from 1R buffer (serial and parallel), sorted by value.
*/
void prepare2RBuffer();
/**
* Check if there is a better 3 R solution than previous one using only two components.
*
* @param aSize gives the number of available combinations to be checked inside
* m_combined_table. Therefore m_combined_table is combined with the primary
* E-series look up table. The 3R result with smallest deviation will be saved
* in results if better than 2R
* Find in 2R buffer two values nearest to the given value (one smaller and one larger).
* It always returns two valid values, even for input out of range or Nan.
*/
void combine3( uint32_t aSize );
std::pair<RESISTANCE&, RESISTANCE&> findIn2RBuffer( double aTargetValue );
/**
* Check if there is a better four component solution.
*
* @param aSsize gives the number of 2R combinations to be checked inside m_combined_table
* Occupied calculation time depends from number of available E-series values with the power
* of 4 why execution for E12 is conditional with 4R check box for the case the previously
* found 3R solution is already exact
* Calculate the best combination consisting of exactly 2, 3 or 4 resistors.
*/
void combine4( uint32_t aSize );
/*
* Strip redundant braces from three component result
*
* Example: R1+(R2+R3) become R1+R2+R3
* and R1|(R2|R3) become R1|R2|R3
* while R1+(R2|R3) or (R1+R2)|R3) remains untouched
*/
void strip3();
/*
* Strip redundant braces from four component result
*
* Example: (R1+R2)+(R3+R4) become R1+R2+R3+R4
* and (R1|R2)|(R2|R3) become R1|R2|R3|R4
* while (R1+R2)|(R3+R4) remains untouched
*/
void strip4();
RESISTANCE calculate2RSolution();
RESISTANCE calculate3RSolution();
RESISTANCE calculate4RSolution();
private:
std::vector<std::vector<R_DATA>> m_tables;
std::vector<std::vector<RESISTANCE>> m_e_series;
std::vector<bool> m_exclude_mask;
std::vector<RESISTANCE> m_buffer_1R;
std::vector<RESISTANCE> m_buffer_2R;
/* Note: intermediate calculations use m_combined_table
* if the biggest list is En, reserved array size should be 2*En*En of std::vector primary list.
* 2 component combinations including redundant swappable terms are for the moment
* ( using values between 10 ohms and 1Mohm )
* 72 combinations for E1
* 512 combinations for E3
* 1922 combinations for E6
* 7442 combinations for E12
* 29282 combinations for E24
*/
std::vector<R_DATA> m_combined_table; // intermediate 2R combinations
uint32_t m_series = ESERIES::E6;
double m_target = 0;
std::array<R_DATA, S4R + 1> m_results; // 2R, 3R and 4R results
uint32_t m_series = ESERIES::E6; // Radio Button State
double m_required_value = 0.0; // required Resistor
};
std::array<std::optional<RESISTANCE>, NUMBER_OF_LEVELS> m_results;
};