MST Logic

Motor Selection Logic

Core Logic Functions

The core logic functions of the selection tool work together to provide the main functionality of the app. Let's walk through the overall flow of the code to see how we filter motors and display results. For now, we'll ignore the energy cost calculation component of the app. Don't worry, we'll get to that later.

When the user submits their specification (power, units, and RPM), the calculateTorque() function is called. This function first checks if the input is valid, then calculates torque based on user input. The calculated torque, input power, input units and input rpm are then set in the app's state.

Filtering, Scoring, and Sorting

This change in the app's state triggers the filterScore() function, which populates a list of compatible results from the motor database. filterScore() does this by filtering the database motors based on user input.

  • This filter is a hard cutoff for torque and power. If a motor's power or torque is less than the user input, the motor is removed from the results.
  • Motors are removed from the results if the motor's minimum rpm is greater than input rpm, or if the input rpm is 20% or more over the motor's rated rpm.
  • For the search algorithm, we omit motors from the results if the input rpm is 30% or lower than their rated RPM, although all our motors can technically operate at speeds as low as 100 RPM.
app.js
results = sortByArg(filterScore(Motors.slice(), power, units, rpm), currentArg, sortOrder)

Results are then scored by comparing the weighted average of rated torque, rpm, and power to the input specification.

filterscore.js
motor.score =
  (relDiff(rpm, motor.rated_rpm) +
    relDiff(power, motor.power) +
    relDiff(torque, motor.torque_nm) * 2) /
  4

As you can see, torque is twice as heavily weighted as the other parameters. Each motor is assigned a score based on the user input using this method. For reference, here's the relative difference function:

relDiff.js
const relDiff = (a, b) => 100 * Math.abs((a - b) / ((a + b) / 2))

Motors are sorted by their computed score by default. The sortByArg() function handles this sorting.

Displaying Results

The handleDisplayed() function is called after the results are filtered and sorted. This function is responsible for the population of the recommended motor cards that are displayed to the user, as well as setting the display state of the splash and error components. Here are the possible outcomes of handleDisplayed():

  • If the user input matches a motor in our database, we only display that motor.
  • If the user input is valid but does not match a motor in our database, we display a custom motor as well as the highest scoring motor that meets the user's input criteria.
  • If the user input is invalid, we display a toast error message, and the splash image remains visible.
  • If the torque is out of bounds, we display a larger error message which explains why no motors were found.

Custom Motor Generation

To generate a custom motor, the handleDisplayed() function uses the standard recommendation as a 'parent' motor. The custom motor will always use the same frame as the parent motor, but can be modified to meet the user's input criteria. Since these custom motors are essentially standard motors running at different operating points, we can calculate an efficiency estimate for the generated motor by fetching the parent motor's efficiency value at the input specification.

Data Fetching

The efficiency fetching is performed by the getCustomEfficiency() function. For each of our base motors, we have extensive dyno data which covers a range of different operating points. The getCustomEfficiency() function uses this data to interpolate the efficiency of the parent motor at the input specification.

Graph Generation

The graph data for each motor is not dynamically generated, but is instead pre-calculated and stored in the database. Within the displayed motor card, there is some simple functionality that matches the correct graph data to the displayed motor. The graphs are generated using recharts.js (opens in a new tab).

Additional Results Table

app.js
const tableResults = removeLargerFrames(results, displayed[1].catnumber.slice(3, 6)) : []

The additional results table is populated by a pruned list of all motors that are compatible with the input specification. This pruning operation consists of first removing the motor which is already displayed in the recommended motor card. Then, we remove all motors with a frame more than one size larger than the recommended motor. This prevents us from including a motor that is significantly larger than what is required.

app.js
<Table
  results={tableResults.filter((result) => {
    return !displayed.find((obj) => obj.catnumber === result.motor)
  })}
  onOrderChange={(e) => {
    setSortOrder(!sortOrder)
    setCurrentArg(e.currentTarget.value)
  }}
/>

PDF Generation

Pdfs are generated using the pdfMe (opens in a new tab) library.

Energy Cost calculation

The energy cost calculation component computes various energy costs based on different input parameters. The main functions that constitute this component include:

  • getInputPower()
  • calculateEfficienciesForMotor()
  • calculateInputPowerArray()
  • calculateWeightedInputPower()
  • calculateEnergyCosts()

Each of these functions plays a key role in the calculation of the energy costs. Let's dig into each function.

getInputPower()

The getInputPower() function calculates the required raw input power given output power (p) and efficiency (e). For example, if a motor is 90% efficient and requires 10 kW of output power, the input power would be 11.11 kW.

getInputPower.js
function getInputPower(p, e) {
  return (p * 745.7) / (e / 100) / 1000
}

This getInputPower() function will then be called by calculateInputPowerArray() at each load percentage to get an array of input power values for a given motor.

calculateInputPowerArray()

For each motor, this function calculates an array of input power values for each load percentage.

  • First, it utilizes the preset load percentages: [30%, 50%, 75%, 100%] and the corresponding affinity curve values: [0.027, 0.125, 0.424, 1] to compute the output power at each load level.
  • Then, the calculateEfficienciesForMotor() function fetches an efficiency value from the appropriate dyno datasheet for each power input.
  • Lastly, getInputPower() can be called with the computed output power and fetched efficiency for each load percentage, and we return an array of input power values.
calculateInputPowerArray.js
inputPowerArrayForMotor = customEfficiencyObjects.map((obj) => {
  const outputPower = obj.adjustedPower
  const efficiency = obj.effData
  return getInputPower(outputPower, efficiency)
})

calculateWeightedInputPower()

This function calculates the weighted input power for a given input power array and distribution. The distribution[] array is populated by the user, in the "input load distribution" modal. The function multiplies each value in the input power array by the corresponding weight in the distribution array, and sums up these weighted values. The result is the average kwH usage of that motor running at the user's spec.

weightedInputPower.js
const calculateWeightedInputPower = (inputPowerArray, distribution) => {
  const distributionArray = Object.values(distribution)
  return inputPowerArray.map((kwhPerHour) => {
    const weightedInputPower = kwhPerHour.reduce((sum, value, index) => {
      const weight = distributionArray[index] / 100
      const weightedValue = value * weight
      return sum + weightedValue
    }, 0)
    return weightedInputPower
  })
}

calculateEnergyCosts()

Finally, this main function ties all the helper functions together, and populates an array of costs for each motor. We make kwH / year, cost per day, cost per week, and cost per year readily available to the UI, although we currently only display kwH / year and cost / year. This calculation is quite easy, since we've made our weightedInputPower[] array represent average kilowatt hours per hour. The values in this array can simply be multiplied by 24 to get daily cost, 168 to get weekly cost, etc.

calculateEnergyCosts.js
const calculateEnergyCosts = () => {
  const rpmPercentages = [0.3, 0.5, 0.75, 1] // 30%, 50%, 75%, and 100% of rated rpm
  const affinityCurve = [0.027, 0.125, 0.424, 1] // corresponding power values
 
  const inputPowerArray = calculateInputPowerArray(displayed, rpmPercentages, affinityCurve) ?? []
  const weightedInputPowerArray = calculateWeightedInputPower(inputPowerArray, distribution) ?? []
 
  setCalculatedCosts([
    // Custom result calculations...
    {
      kpyr: roundHelper(weightedInputPowerArray[0] * hpd * dpw * 52),
      cpd: roundHelper(weightedInputPowerArray[0] * cost * hpd),
      cpw: roundHelper(weightedInputPowerArray[0] * cost * hpd * dpw),
      cpy: roundHelper(weightedInputPowerArray[0] * cost * hpd * dpw * 52),
    },
    // Standard result calculations...
    {
      kpyr: roundHelper(weightedInputPowerArray[1] * hpd * dpw * 52),
      cpd: roundHelper(weightedInputPowerArray[1] * cost * hpd),
      cpw: roundHelper(weightedInputPowerArray[1] * cost * hpd * dpw),
      cpy: roundHelper(weightedInputPowerArray[1] * cost * hpd * dpw * 52),
    },
  ])
}