A Python/PyQt6 app to pack circular wires (with arbitrary diameters) into the smallest possible circular bundle.
It supports multi-start optimization, shielding layers, and live plotting.
- Features
- Installation & Run
- UI Walkthrough
- Predefined Sizes (
wire_types.yaml
) - How it Works (Math)
- Tips & Troubleshooting
- Screenshots
- Define any number of wire types (custom or from predefined sizes via YAML).
- Color-coded wires.
- Manufacturing margin (percentage) inflates radii to enforce spacing.
- Multi-start SLSQP with parallel runs to escape local minima.
- Inner exclusion constraint from prior shields (no wire can cross a shield).
- Live plot:
- Wires placement
- Dashed current outer boundary
- Grey shield rings
You can run the app in two ways:
Option 1: Using uv (recommended)
uv run main.py
This automatically manages a virtual environment and dependencies.
First install dependencies:
pip install PyQt6 numpy scipy joblib pyyaml
Then run:
python main.py
-
1. Define Wire Types
-
Set Count and Wire Diameter:
- Custom: enter diameter in mm.
- Predefined Sizes: pick a label defined in
wire_types.yaml
.
-
Pick a Color and click Add Wire. Identical (color + diameter) entries merge counts automatically.
-
-
2. Optimization Parameters
- Number of Solver Initializations: more restarts → better layout, slower.
- Max Solver Iterations: SLSQP cap per run.
- Manufacturing Tolerance Margin: extra spacing (percent) added to radii.
-
3. Defined Wires
- Shows the current working set.
- Remove Selected Wire or Clear All (clears wires, layers, and results).
-
4. Shielding
-
Set Shield thickness (mm).
-
Add Shielding is enabled only after a successful optimize. When clicked:
- The current optimized bundle becomes a locked layer with a shield of the given thickness.
- The inner exclusion radius is updated.
- The working wire list is cleared for defining the next ring.
-
-
Optimize and Plot
-
Runs the multi-start optimizer and updates:
- Plot (including historic layers)
- Outer diameter in mm and inches.
-
-
Results
- The plot is centered and scaled to fit.
- The window is scrollable if content exceeds the viewport.
Place this file next to main.py
. It maps labels → diameter (mm):
# Example — adapt to your catalog
"16 AWG": 1.291
"18 AWG": 1.024
"20 AWG": 0.812
"1.00 mm": 1.000
"0.90 mm": 0.900
"Ethernet": 2.000
In the UI, choose Predefined Sizes to select one of these keys.
Minimize the enclosing radius
Variables
- Centers
$(x_i, y_i)$ for$i = 1..n$ - Outer radius
$R$
Effective radii
Constraints
-
Containment:
$|c_i| + r_i^{\mathrm{eff}} \le R$ -
Non-overlap:
$|c_i - c_j| \ge r_i^{\mathrm{eff}} + r_j^{\mathrm{eff}}$ -
Frozen core (from shields):
$|c_i| \ge R_{\text{core}} + r_i^{\mathrm{eff}}$
Objective
Minimize
- If optimization is slow, reduce initializations or max iterations.
- If wires overlap visually, increase margin a bit (e.g., 1–3%).
- Add Shielding stays disabled until a valid solution is available.
- Clear All resets everything (wires, layers, results, frozen core).
This is the main window where you define wires, optimization parameters, and shielding layers:
After optimization and adding shielding, the layout will look like this: