Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stats): collect drill stats #373

Open
wants to merge 20 commits into
base: v5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@tracespace/plotter": "workspace:*",
"@tracespace/www": "workspace:*",
"@tracespace/xml-id": "workspace:*",
"@tracespace/stats": "workspace:*",
"whats-that-gerber": "workspace:*"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/stats/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.tsbuildinfo
__tests__
18 changes: 18 additions & 0 deletions packages/stats/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# @tracespace/stats

A drill stats collector for [@tracespace/parser][] ASTs.

Part of the [tracespace][] collection of PCB visualization tools.

**This package is still in development and is not yet published.**

## usage

```js
import {createParser} from '@tracespace/parser'
import {collectDrillStats} from '@tracespace/stats'

const syntaxTree = createParser().feed(/* ...some gerber string... */).result()

const stats = collectDrillStats([syntaxTree])
```
50 changes: 50 additions & 0 deletions packages/stats/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@tracespace/stats",
"publishConfig": {
"access": "public"
},
"version": "0.0.0-unreleased",
"description": "Collect drill statistics for PCB fabrication files.",
"main": "./dist/tracespace-plotter.umd.cjs",
"module": "./dist/tracespace-plotter.es.js",
"types": "./lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"source": "./src/index.ts",
"import": "./dist/tracespace-stats.es.js",
"require": "./dist/tracespace-stats.umd.cjs"
}
},
"type": "module",
"sideEffects": false,
"repository": {
"type": "git",
"url": "git+https://github.com/tracespace/tracespace.git",
"directory": "packages/stats"
},
"scripts": {
"build": "vite build",
"clean": "rimraf dist"
},
"keywords": [
"gerber",
"excellon",
"pcb",
"circuit",
"hardware",
"electronics"
],
"contributors": [
"KN4CK3R <[email protected]> (https://www.oldschoolhack.me)",
"Mike Cousins <[email protected]> (https://mike.cousins.io)"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/tracespace/tracespace/issues"
},
"homepage": "https://github.com/tracespace/tracespace#readme",
"dependencies": {
"@tracespace/parser": "workspace:*"
}
}
41 changes: 41 additions & 0 deletions packages/stats/src/__tests__/stats.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {describe, it, expect} from 'vitest'
import {createParser} from '@tracespace/parser'
import {collectDrillStats, DrillStats} from '..'

describe('@tracespace/stats', () => {
it('should return an empty result', () => {
const expected: DrillStats = {
drillHits: [],
drillRoutes: [],
totalDrills: 0,
totalRoutes: 0,
minDrillSize: 0,
maxDrillSize: 0,
}

expect(collectDrillStats([])).to.eql(expected)
})

it('should collect drill stats', () => {
const parser = createParser()
parser.feed(
'M48\nM72\nINCH,TZ\nT01C0.0240\nT02C0.0335\n%\nG90\nT01\nX16910Y10810\nX15010Y12410\nX15100Y14300\nT02\nX26450Y10700\nT1\nG00X0Y0\nG01X2500Y2500\nG01X5000Y0\nM30\n'
)
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved

const tree = parser.result()

const expected: DrillStats = {
drillHits: [
{count: 3, diameter: 0.024},
{count: 1, diameter: 0.0335},
],
drillRoutes: [{count: 2, diameter: 0.024}],
totalDrills: 4,
totalRoutes: 2,
minDrillSize: 0.024,
maxDrillSize: 0.0335,
}

expect(collectDrillStats([tree])).to.eql(expected)
})
})
159 changes: 159 additions & 0 deletions packages/stats/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as Parser from '@tracespace/parser'

export interface DrillUsage {
diameter: number
count: number
}

export interface DrillStats {
drillHits: DrillUsage[]
drillRoutes: DrillUsage[]
totalDrills: number
totalRoutes: number
minDrillSize: number
maxDrillSize: number
}

/**
* Method to collect drill stats of parsed drill files.
*
* @example
* ```ts
* import {createParser} from '@tracespace/parser'
* import {collectDrillStats} from '@tracespace/stats'
*
* const parser = createParser()
*
* parser.feed(...)
*
* const tree = parser.results()
* const stats = collectDrillStats([tree])
* ```
*
* @category Stats
*/
export function collectDrillStats(trees: Parser.Root[]): DrillStats {
const state: DrillStatsState = {
usedTools: {},
drillsPerTool: {},
routesPerTool: {},
minDrillSize: null,
maxDrillSize: null,
}

for (const tree of trees) {
if (tree.filetype !== Parser.DRILL) {
continue
}

_updateDrillStats(state, tree)
Copy link
Member

@mcous mcous May 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions that mutate an input parameter to do their main work are usually a bad idea. I would solve this problem a different way, perhaps with a function that takes a single tree and returns the stats and another function that combines the stats

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that too but the merging bloats the whole thing for not much value:

function _mergeStates(
  s1: DrillStatsState,
  s2: DrillStatsState
): DrillStatsState {
  return {
    usedTools: {...s1.usedTools, ...s2.usedTools},
    drillsPerTool: _mergeAdd(s1.drillsPerTool, s2.drillsPerTool),
    routesPerTool: _mergeAdd(s1.routesPerTool, s2.routesPerTool),
    totalDrills: s1.totalDrills + s2.totalDrills,
    totalRoutes: s2.totalRoutes + s2.totalRoutes,
    minDrillSize: _cmp(s1.minDrillSize, s2.minDrillSize, Math.min),
    maxDrillSize: _cmp(s1.maxDrillSize, s2.maxDrillSize, Math.max),
  }

  function _mergeAdd(
    r1: Record<string, number>,
    r2: Record<string, number>
  ): Record<string, number> {
    const rec = {...r1}
    for (const [key, count] of Object.entries(r2)) {
      const oldCount = rec[key] ?? 0
      rec[key] = oldCount + count
    }

    return rec
  }

  function _cmp(
    n1: number | null,
    n2: number | null,
    cmp: (...values: number[]) => number
  ): number | null {
    if (n1 === null && n2 === null) {
      return null
    }

    if (n1 === null) {
      return n2
    }

    if (n2 === null) {
      return n1
    }

    return cmp(n1, n2)
  }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that DrillStatsState doesn't lend itself to merging, but it's also a completely internal interface. If DrillStatsState's shape is such that it leads to doing things the way _updateDrillStats is currently written, I think DrillStatsState should be changed

}

let totalDrills = 0
const drillHits: DrillUsage[] = []
for (const [key, count] of Object.entries(state.drillsPerTool)) {
totalDrills += count
drillHits.push({
diameter: state.usedTools[key],
count,
})
}
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved

let totalRoutes = 0
const drillRoutes: DrillUsage[] = []
for (const [key, count] of Object.entries(state.routesPerTool)) {
totalRoutes += count
drillRoutes.push({
diameter: state.usedTools[key],
count,
})
}

const stats: DrillStats = {
drillHits,
drillRoutes,
totalDrills,
totalRoutes,
minDrillSize: state.minDrillSize ?? 0,
maxDrillSize: state.maxDrillSize ?? 0,
}

return stats
}

interface DrillStatsState {
usedTools: Record<string, number>
drillsPerTool: Record<string, number>
routesPerTool: Record<string, number>
minDrillSize: number | null
maxDrillSize: number | null
}

function _updateDrillStats(state: DrillStatsState, tree: Parser.Root) {
let currentTool = null
let currentMode: Parser.InterpolateModeType = Parser.DRILL
for (const node of tree.children) {
switch (node.type) {
case Parser.TOOL_DEFINITION: {
currentTool = node.code

if (node.shape.type !== Parser.CIRCLE) {
continue
}
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved

const {diameter} = node.shape

state.usedTools[currentTool] = diameter

if (state.minDrillSize === null || diameter < state.minDrillSize) {
state.minDrillSize = diameter
}

if (state.maxDrillSize === null || diameter > state.maxDrillSize) {
state.maxDrillSize = diameter
}
KN4CK3R marked this conversation as resolved.
Show resolved Hide resolved

break
}

case Parser.TOOL_CHANGE:
currentTool = node.code
break
case Parser.INTERPOLATE_MODE:
currentMode = node.mode
break
case Parser.GRAPHIC:
if (currentTool === null) {
continue
}

if (node.graphic === null) {
switch (currentMode) {
case Parser.DRILL: {
const drillCount = state.drillsPerTool[currentTool] ?? 0
state.drillsPerTool[currentTool] = drillCount + 1
break
}

case Parser.LINE:
case Parser.CW_ARC:
case Parser.CCW_ARC: {
const routeCount = state.routesPerTool[currentTool] ?? 0
state.routesPerTool[currentTool] = routeCount + 1
break
}

default:
break
}
} else if (node.graphic === Parser.SLOT) {
const routeCount = state.routesPerTool[currentTool] ?? 0
state.routesPerTool[currentTool] = routeCount + 1
}

break
default:
break
}
}
}
11 changes: 11 additions & 0 deletions packages/stats/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../config/tsconfig.base.json",
"references": [{"path": "../parser"}],
"compilerOptions": {
"composite": true,
"emitDeclarationOnly": true,
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"]
}
22 changes: 22 additions & 0 deletions packages/stats/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {defineConfig} from 'vite'

import {baseConfig, libraryFilename} from '../../config/vite.config.base'

export default defineConfig({
...baseConfig,
build: {
lib: {
entry: 'src/index.ts',
name: 'TracespaceStats',
fileName: libraryFilename('tracespace-stats'),
},
rollupOptions: {
external: ['@tracespace/parser'],
output: {
globals: {
'@tracespace/parser': 'TracespaceParser',
},
},
},
},
})
Loading