diff --git a/pcb_calculator/calculator_panels/panel_r_calculator.cpp b/pcb_calculator/calculator_panels/panel_r_calculator.cpp index 0de6253672..4c7c8b4517 100644 --- a/pcb_calculator/calculator_panels/panel_r_calculator.cpp +++ b/pcb_calculator/calculator_panels/panel_r_calculator.cpp @@ -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 ); } diff --git a/pcb_calculator/resistor_substitution_utils.cpp b/pcb_calculator/resistor_substitution_utils.cpp index 6d3e25ec5b..540d93432a 100644 --- a/pcb_calculator/resistor_substitution_utils.cpp +++ b/pcb_calculator/resistor_substitution_utils.cpp @@ -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(); +} diff --git a/pcb_calculator/resistor_substitution_utils.h b/pcb_calculator/resistor_substitution_utils.h index 0240d64bb3..3095359904 100644 --- a/pcb_calculator/resistor_substitution_utils.h +++ b/pcb_calculator/resistor_substitution_utils.h @@ -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; +}; \ No newline at end of file