Skip to content

Releases: medinstech/perfstudio

PerfStudio v0.6.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 17:04

Added

  • A pin on an edge-connector finger is a DRC error (edge-connector-conflict), the
    third member of the family with mounting-hole-conflict and cut-track-conflict and
    the most absolute of the three: a bore and a cut each destroy the pad and leave a hole,
    but a finger is solid copper that was never drilled (geometry.undrilled_holes), so a
    through-hole lead cannot be fitted there at all. Nothing checked it, so a part dropped on
    the finger strip was accepted in silence — and the finger strip runs along the board
    edge, which is exactly where a connector or a terminal block gets placed.

Changed

  • The placement ghost goes red over a hole with no pad. A mounting bore, an
    edge-connector finger and a cut track all leave a position on the grid with nothing to
    solder a lead into, and DRC has always called each of them an error — but the ghost
    stayed green right up to the click, so the part went down and the only thing that ever
    said so was a line in the DRC panel afterwards. The status line now names it too. The
    placement is still not refused: a mounting hole can be added over a part that was
    already there, so refusing would only make the same board harder to reach while leaving
    it reachable.

  • The blank-board guidance appears until you have placed your first part, then stops.
    It is for the first launch; repeating it on every launch afterwards is the application
    explaining its own front door to somebody who has been through it a hundred times — and
    there was nowhere to click it away, because the block is transparent to the mouse by
    design.

  • Two solder traces running side by side are one row in the findings panel, not
    sixteen.
    solder-trace-proximity fires once per pad per trace, so eight pads of
    perfectly ordinary parallel routing put sixteen copies of one sentence in the panel and
    pushed everything else off the bottom — which is how the most valuable rule in the tool
    became the one people scroll past. Gathered by the run now (C4–J4 · 8 pads), with
    every pad still there one level down.

    • The rule itself is unchanged, and deliberately: it is a warning, not an error —
      nothing is refused and the board is legal — and DRC's output is compared byte-for-byte
      against the reference implementation this port is proved against
      (test_matches_typescript_golden_drc). Grouping in the engine was written, measured
      at 8 findings → 4 on random-09, and reverted for that reason. The noise was never in
      the engine; it was in the panel.
  • Auto-place stops turning parts for nothing. The annealer accepts any move whose
    cost delta is <= 0, and a rotation's delta is exactly zero for every part the cost
    function cannot tell apart turned — one on no net, or one whose courtyard is square. So
    it turned parts for no reason at all: on the dense fixture it turned 11, and 5 of
    those cost 0.00 to turn back. That is not free to whoever is holding the iron — every
    rotation is an orientation to get right at the bench and a polarity line in the build
    guide, and a plan reading "11 turned" describes work the tool did rather than noise it
    made. When the placer has no preference, the user's own orientation is now the one
    kept: across eight fixtures and eight seeds, parts turned went from 46 to 30 with the
    routed cost identical to the last decimal
    .

    • Applied to the WINNER only, after _pick_best has chosen, and checked against the
      router. Tidying every candidate before the choice changes the boards the router is
      shown and therefore which one wins — measured on dense, where doing it that way
      left the mean routed cost 31.5 → 35.9 while the best was unchanged.
    • Letting a rotation nudge the anchor at the same time — the obvious fix for a part
      that can only be turned if it also moves a hole or two — was tried and measured
      worse
      : summed mean routed cost 254.1 → 261.0 at half the rotations nudging, 260.0 at
      a quarter, 262.6 at 0.15, with ne555 losing most of it. That result is written down at
      the proposal site so nobody re-derives it.

Windows - PerfStudio_..._Setup.exe. It is unsigned, so SmartScreen will
warn on first run: More info -> Run anyway. A signed build is planned.

Linux - perfstudio-...-x86_64.AppImage. chmod +x and run it; it needs
glibc 2.35 or newer. If it will not start, the machine has no FUSE 2: install
libfuse2, or run the file with --appimage-extract-and-run, which unpacks it
instead of mounting it.

macOS - perfstudio-...-arm64.dmg, Apple silicon. Drag it to Applications,
then run this before opening it:

xattr -dr com.apple.quarantine /Applications/PerfStudio.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first launch.
On macOS 15 and later the dialog it raises defaults to Move to Trash, which
deletes the install. Clearing the flag first avoids the dialog entirely. On an
Intel Mac, install from source.

Anywhere with Python 3.12+: pip install -e . from a clone, then
perfstudio.

This is pre-alpha software. Nobody has yet built a real board by following a
generated guide - check every measurement against the part in your hand.

PerfStudio v0.5.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 15:44

Added

  • A part can be given a value (double-click it, or F2). This was the one field on
    the document no human could reach: every part the window placed was created with
    value="" and nothing could change it afterwards, while an agent on the MCP server has
    been able to pass one to place_component since that server existed. guide._bom
    groups on exactly this field, so the tool's own build guide printed "Resistor × 4"
    where it meant "10k × 4".

    • The dialog carries only what component.update carries — reference, value, lock.
      Rotation is a command of its own, and putting it here would make one press of OK into
      two entries on the undo stack for what the user experienced as one edit.
    • The Parts panel has a value box as well, applied to each part as it is placed. A
      board is populated in runs — five 10k resistors, then three 100nF — and naming each
      one afterwards through its own dialog is the same work done once per part instead of
      once per run.
  • Right-click menus on the board, the Nets panel and the findings panel. There were
    none anywhere: everything a part can be told to do lived in the menu bar at the top of a
    window a metre wide, while the part itself was under the pointer in the middle of the
    board. Every menu is built from the same QAction objects the menu bar holds, so an
    action greyed out in one place is greyed out in the other. Right-click already means
    "finish" inside a board mode, and the guards for that are described in
    BoardView.contextMenuEvent.

  • The window remembers its layout — geometry, dock sizes and positions, board colour,
    the ratsnest, ruler and hatch toggles, and the preferred connection style. All of it
    reset on every launch, and the cost was paid by whoever used the tool most. The 3D panel
    deliberately still starts closed: restoring it open would build VTK's whole pipeline
    during startup to show a board nobody has looked at yet.

  • The build guide, in the window (Ctrl+4). The soldering order is the thing this
    application is for, and the only way to see one was to export four files and go and
    find them — so the order the tool had worked out was invisible while the board was being
    designed, which is when it is worth knowing. Picking a step selects its parts on the 2D
    board, brings its holes into view, and moves the 3D assembly slider with it. Closed by
    default and rebuilt only while open, because building a guide runs DRC and LVS.

  • The export offers to open what it wrote, rather than ending at a line in the status
    bar naming a file in a directory the user then has to go and find.

  • A language menu (View ▸ Language). The Turkish catalogue has existed the whole time
    and could be selected only by an environment variable or a command-line flag. Applied at
    the next start, and it says so: every label is translated once as the window is built,
    and the widgets a live rebuild missed would be exactly the ones nobody would notice had
    stayed English.

  • Files can be dropped on the window — a .perf to open it (through the same
    unsaved-work guard the Open menu item uses) or a .net to import it. Nothing happened
    before, which reads as the application refusing that kind of file rather than refusing
    drops.

  • The findings panel has a filter box, like the parts and nets panels. A board
    mid-layout carries a hundred proximity warnings, and "show me the errors" is how anybody
    reads a list that long.

  • Copy, paste and duplicate a block of board (Ctrl+C / Ctrl+V / Ctrl+D). A
    perfboard project repeats itself in a way a PCB does not — eight identical channels,
    the same RC pair at every op-amp — and until now the only way to build the second one
    was to place every part again by hand.

    • A block is parts AND the copper between them, placed by one new command,
      block.place. Two commands would put a state on the undo stack nobody chose: the
      parts down with their wiring gone, one Ctrl+Z from a board that looks finished and
      is not. The copper is validated against a document the new parts have already
      joined, which is what lets a pasted lead bend name the part it is a leg of.
    • JSON on the system clipboard, not a variable on the window, so a block crosses
      documents and crosses two running copies of the application — which is the "channel 1
      into that other board" case. It is readable, so a block can be pasted into a bug
      report, for the same reason the project file is diffable.
    • A copy of R1 is not R1. Pasted parts get fresh references counted from the board,
      and pasted copper carries no net claim: copper that kept net_id would tell LVS
      the new block is wired to a schematic that has never heard of it. Unclaimed copper is
      also the one kind rip-up and the stale-conductor cleanup both promise never to touch.
    • Three things are deliberately left behind, and each is reported rather than silent: a
      lead bend whose part was not in the selection (it would be a leg of nothing), the
      lock (a pasted part is one you are still positioning), and copper that the offset
      would have pushed off the board.
    • Duplicate does not touch the system clipboard. Duplicating a part is a board
      operation; it has no business throwing away what somebody copied elsewhere.
  • Stripboard is a board type this application can actually design on, rather than a
    string the data model accepted and nothing understood. board.type and strip_axis
    have been in model.py, persist.py and the .perf format since the first commit, a
    cut.add command has existed the whole time, and nothing read any of it: the
    connectivity engine did not know a strip joins the holes along it, so a stripboard
    loaded as a pad-per-hole board with a decorative field set.

    • A cut destroys the copper AT a hole (stripboard.py). That is how a track is
      actually broken — a spot-face cutter or a drill bit turned by hand in the hole, which
      takes the pad with it. The alternative model, a cut between two holes, describes a
      knife scored across the track: a real technique, much harder to do reliably at 2.54 mm
      and much harder to inspect, and one that would leave a cut without an address in an
      application where every message is addressed.
    • Connectivity gained a fourth rule beside the three it has always had: on
      stripboard the BOARD joins holes, and nobody soldered those connections. Only holes
      something is soldered into take part — a strip physically joins all thirty holes in
      its row, and registering the twenty-six nobody used would put every empty pad on the
      board into a net, which is exactly what the module's existing note says not to do.
      Gated on the board type, which is why fifteen golden fixtures still reproduce byte
      for byte.
    • The autorouter for a stripboard subtracts before it adds (striproute.py). Two
      pins of different nets on one strip are shorted by the board itself, so the first
      pass is cuts and the second is links; the links go over the COMPONENT side, because
      the solder side is one sheet of parallel copper and a wire laid across it there shorts
      every strip it crosses. Pins it cannot separate — adjacent, with no hole between them
      to drill — are reported by name rather than routed around, because the fix is to move
      a part and that is the user's decision.
    • The cuts and the links commit as one command (stripboard.apply). Separately,
      one Ctrl+Z leaves a board cut apart with nothing linking it, or linked with nothing
      cut — which is a short across two nets, and a state nobody designed.
    • DRC reports a pin standing in a cut hole (cut-track-conflict), an error for the
      same reason mounting-hole-conflict is: the pad is gone, so the board cannot work
      rather than probably will not.
    • Both renderers draw the strips, and the 2D view marks each cut. A stripboard drawn
      as a grid of separate pads is a picture of a different board, on a view whose whole
      job is to be checked against the real one.
    • The build guide cuts first and measures each one. The cuts are made from the
      copper side with a drill, and once a part is over a hole there is no way back to it —
      so they are phase 0, and each gets a blocking isolation probe, because a cut that did
      not go all the way through looks exactly like one that did.
    • Board Setup chooses the type and which way the strips run, and Draw ▸ Cut Track
      (X) makes and un-makes cuts by clicking. Not done: the placer does not yet reward
      putting a net's pins on one strip, which on stripboard is most of the design — said
      here rather than left to be discovered.
  • Something now checks what the render looks like (PLAN.md §10 asks for visual
    regression). The headless run has always produced PNGs and CI has always kept them as
    artefacts, and the only thing asserted about one was that it began with the PNG magic
    bytes — so a render that lost every pad, drew the board inside out or came out blank
    passed the build, and somebody had to open the artefact and look.

    • Not a pixel diff. Antialiasing and Qt's own version move individual pixels across
      platforms and this suite runs on three; a per-pixel golden would fail on macOS for
      reasons that have nothing to do with the board, and a test that fails for the wrong
      reason gets switched off. tests/test_render_golden.py compares the mean colour of
      each cell of a 6 × 6 grid
      against a checked-in signature: re-rendering the board
      moves it by 0.0 of 255, rendering the other face by 27.5, and rendering it with every
      part and conductor removed by 22.6 — against a tolerance of 3.
    • The first attempt measured how much of each cell...
Read more

PerfStudio v0.4.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 09:21

Added

  • Installers, on all three platforms, from one PyInstaller spec. Pushing a v* tag
    builds a Windows installer, a Linux AppImage and a macOS disk image, smoke-tests each
    one by unpacking it and asking the binary inside what version it is, and attaches them
    to the release. Nothing has been tagged yet, so nothing has been published.

    • The tag is checked against the source rather than trusted. A v0.4.0 tag on a
      tree that still says 0.4.0.dev0 publishes an installer that disagrees with its own
      file properties, and nothing downstream would notice — so the workflow refuses that,
      refuses a version that still carries a .devN suffix, and refuses one with no
      CHANGELOG.md section. The release notes are that section, so the two cannot say
      different things about what shipped.
    • Nothing is code-signed, and the release notes say so along with the click-through
      each platform needs. A Windows EV certificate is ~$300/year and Apple notarization
      $99/year (PLAN.md §12). The macOS bundle is signed ad-hoc, which is not a trust
      decision — it is the minimum Apple silicon will execute at all.
    • Ubuntu 22.04 and macOS 15 are pinned rather than -latest: a PyInstaller bundle
      carries Python and Qt but links against the host's glibc, and glibc is forward
      compatible only, so a 24.04 build would require 2.39 and rule out Debian 12 and every
      enterprise distribution still in service.
    • The Windows installer speaks English and Turkish, because the application does,
      and an installer that could only speak English would be the one part of the product
      that does not. All three platforms claim .perf, so double-clicking a board opens it.
  • Four example circuits, each shipped as both the netlist and the finished board.
    There was one netlist and no board. examples/ now carries the NE555 astable, an LM317
    adjustable supply, a one-transistor guitar booster and an Arduino I/O shield — the
    .net a schematic tool exports, and the .perf that importing, placing and routing it
    produces.

    • Chosen for what they make the tool say rather than for variety. The LM317 is a
      TO-220, so heat-proximity has a hot part to measure from, and that board carries
      the set's one solder-trace-proximity warning — R5' doing its job and becoming a
      measurement checkpoint. The booster is on FR-2, so the guide drops the iron 30 °C
      and pad-lifting-risk can fire at all. The shield is two headers, which is where lead
      bends and short traces do nearly all the work.
    • tests/test_examples.py asserts on every commit that all four load without warnings,
      round-trip byte-identically, match their schematics under LVS and carry no DRC error.
      A broken example on the front page is worse than no example.
  • An application icon, drawn in code by tools/make_assets.py and committed as a
    .png and an .ico — because unlike the toolbar, an installer and a .desktop entry
    need real files to point at. Every colour comes from ui/boardcolors, so the mark is
    the same green and the same gold as the board in the editor.

  • A demo animation of the build order (tools/make_demo.py), generated by playing the
    guide back through document_at_step — the same function behind the 3D panel's assembly
    slider, so it cannot show an order the guide does not actually prescribe.

  • Copper on the face you are not looking at is hatched. View ▸ Hatch Copper on the Far Side, on by default. The board is opaque: a solder-side trace drawn solid while you are
    looking at the component side says this is in front of you, which is exactly the
    misreading _paint_body_shadow already exists to prevent for part bodies — and the one
    that gets a board soldered on the wrong face. A conductor and a part now say "I am on the
    other side" the same way, in the one visual word this application already had for it.

    • Stroked into a fillable shape rather than dashed. A dash already means a top jumper
      in CONDUCTOR_STYLE, and giving one mark two meanings costs more than it saves. The
      outline around the hatch is what keeps a run traceable end to end at low zoom, where a
      0.9 mm trace is a few pixels wide and hatching alone cannot read.
    • The joints stay solid. Where a conductor is soldered down does not change with the
      face you look from — the hole goes through the board — and it is what someone counts
      pads against while tracing a run. A test pins that a hatched trace still marks every
      hole it contacts.
    • The hatch brush carries the inverse of the painter transform, so it holds its spacing on
      screen while the board zooms instead of turning solid zoomed in and vanishing zoomed out
      — the same correction the body shadow needs, for the same reason.
    • Off is a real option, not a concession: someone tracing a dense solder side may simply
      want to see it plainly. On is the default because the default has to be the reading that
      cannot mislead.
  • The router can try every style and keep the best. Route ▸ Preferred Connection ▸ Try each and keep the best, style: "best" over MCP. Picking a routing style meant
    guessing — before seeing a single route — whether this particular board comes out better
    with solder or with wire. Planning is pure and a plan is cheap, so the tool can stop
    guessing and measure: it routes the board once per style and keeps the one that is least
    work to build. Costs about two ordinary routes (607 ms against 273 ms on the NE555
    fixture), because balanced's own rip-up passes are the slow ones.

    • It does not compare costs, and that is the whole trick. Each style's plan carries a
      total_cost quoted in that style's own currency: the wire table prices a solder step
      at 4 and an insulated wire at 6, so its plans are cheap by its own definition of
      cheap
      , and min(total_cost) would pick wire on every board ever. The comparison is on
      physical facts instead — traces, wires, millimetres of wire, holes at R5' bridging
      risk — which mean the same thing whichever table produced them. A test asserts
      score_plan never mentions total_cost.
    • An unrouted connection is a gate, not a term. A plan that leaves one can never win
      on being tidier elsewhere, however large the gap: PLAN.md §13 names "it routed most of
      it and left four connections" as the trap every previous perfboard autorouter fell into.
    • A wire costs more to build than a trace, and by a fixed amount before a single
      millimetre exists — measure, cut, strip, tin, dress, solder twice. The first version of
      this scoring priced them the same, which undercharges the one primitive with real
      preparation behind it and makes every comparison meaningless.
    • Every loser is kept and reported, with its measurements, in the status bar's tooltip
      and in comparison over MCP. The winner is chosen on an exchange rate between wires and
      bridging risk that the user is entitled to disagree with, and they cannot disagree with
      numbers they were never shown — so all four styles stay pickable by hand.
    • Ties fall to the earlier style tried, so balanced keeps its place and the sweep
      reduces to today's behaviour when nothing beats it. On both the NE555 and dense
      fixtures, nothing does.
    • Headless prints the whole table, so a change to any cost table shows up in CI as a
      different winner rather than as a silently different board.
  • Resistors wear their colour code, in both views. A resistor is the commonest part on
    almost any board and every one of them was an anonymous beige blob — so "is the 10k in
    the right place"
    could not be answered by looking, which is the one job the 3D view has.
    The bands are decoded from ComponentInstance.value, which is already in the document,
    so nothing new is stored and the bands cannot disagree with the netlist.

    • It never guesses. A wrong band is worse than no band: somebody would read it and
      fit the wrong part. The parser understands the way schematics actually write a
      resistance — 470, 470R, 4R7, 10k, 4k7, 2.2k, 2M2, 1kΩ, where the unit
      letter stands in for the decimal point — and returns nothing at all for anything else.
      100nF, 10uH, 2A, NE555 and dense.perf's placeholder v12 all decode to no
      bands rather than to a plausible resistance, and that is the load-bearing test.
    • A diode is not a resistor, though they share the axial-cylinder archetype. A
      polarized axial body keeps its cathode stripe and is never banded, the same split
      style_for already makes.
    • Both views read bodies.resistor_bands, so the editor and the 3D view cannot print
      different parts. The library icons still draw three generic bands, and correctly: that
      list shows footprints, which have no value to decode — it is a picture of a resistor,
      where the board is a picture of a 10k.
  • Parts are lit like the material they are made of. BodyStyle.metallic and
    BodyStyle.lens were documented as shading hints for both renderers and read by almost
    neither — the HC-49 crystal, the one part in the registry that is literally a metal can,
    hardcoded its own metal shading while carrying the flag that says so, and no LED was ever
    lit as a lens. Two sources of truth for one fact, in the module that exists to prevent
    exactly that. bodies.surface_for is now the single answer, and both views ask it.

    • The 2D view gained a highlight swept across the short axis of a body, because that is
      the direction a cylinder curves in. A flat fill is what made every part read as a
      sticker printed on the board rather than an object standing on it.
    • A LED now looks lit, a crystal can looks like metal, and a DIP sti...
Read more