├── tests ├── __init__.py ├── test_parser.py ├── test_component.py ├── test_device.py ├── test_pinassign.py └── test_circuit.py ├── examples ├── __init__.py ├── .gitignore ├── print_library.py ├── sallen_key │ ├── build.py │ └── sallen_key.py ├── mcu │ ├── build.py │ └── mcu.py ├── common_emitter │ ├── build.py │ └── common_emitter.py ├── build.py ├── joule_thief │ ├── joule_thief.py │ └── build.py └── voltage_divider │ └── voltage_divider.py ├── pycircuit ├── __init__.py ├── library │ ├── old │ │ ├── sifive │ │ │ ├── __init__.py │ │ │ └── fe310g000.py │ │ ├── ti │ │ │ ├── __init__.py │ │ │ ├── tps65982.py │ │ │ ├── tps6229x.py │ │ │ └── tps6213x.py │ │ ├── connectors.py │ │ ├── busses.py │ │ ├── circuits.py │ │ ├── ftdi │ │ │ └── ftdi.py │ │ └── lattice │ │ │ └── lattice.py │ ├── __init__.py │ ├── devices.py │ ├── packages.py │ ├── outlines.py │ ├── components.py │ └── design_rules.py ├── formats │ ├── json.py │ ├── __init__.py │ ├── yosys.py │ ├── kicad.py │ ├── spice.py │ └── svg.py ├── testbench.py ├── optimize.py ├── device.py ├── pinassign.py ├── outline.py ├── build.py ├── component.py ├── layers.py ├── traces.py ├── pcb.py └── package.py ├── MANIFEST.in ├── viewer ├── .gitignore ├── package.json ├── app.js ├── css │ ├── pcb.svg.css │ └── graphviz.svg.css └── index.html ├── placer ├── __init__.py ├── grid.py ├── bin.py ├── place.py └── box.py ├── router ├── __init__.py └── router.py ├── .gitignore ├── Pipfile ├── .travis.yml ├── setup.py ├── README.md └── Pipfile.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pycircuit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.sp -------------------------------------------------------------------------------- /viewer/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /files/ 3 | -------------------------------------------------------------------------------- /placer/__init__.py: -------------------------------------------------------------------------------- 1 | from placer.place import Placer 2 | -------------------------------------------------------------------------------- /router/__init__.py: -------------------------------------------------------------------------------- 1 | from router.router import Router 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | dist/ 3 | MANIFEST 4 | .cache/ 5 | *.pyc -------------------------------------------------------------------------------- /pycircuit/library/old/sifive/__init__.py: -------------------------------------------------------------------------------- 1 | from pycircuit.library.sifive.fe310g000 import * 2 | -------------------------------------------------------------------------------- /pycircuit/library/old/ti/__init__.py: -------------------------------------------------------------------------------- 1 | from pycircuit.library.ti.tps6213x import * 2 | from pycircuit.library.ti.tps6229x import * 3 | -------------------------------------------------------------------------------- /pycircuit/library/old/ti/tps65982.py: -------------------------------------------------------------------------------- 1 | from pycircuit.device import * 2 | 3 | # Communication Controllers 4 | # USB C port controller 5 | Device('TPS65982', Pin('1')) 6 | -------------------------------------------------------------------------------- /pycircuit/library/__init__.py: -------------------------------------------------------------------------------- 1 | from pycircuit.library.components import * 2 | from pycircuit.library.packages import * 3 | from pycircuit.library.devices import * 4 | from pycircuit.library.design_rules import * 5 | from pycircuit.library.outlines import * 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | numpy = "*" 11 | pykicad = "*" 12 | shapely = "*" 13 | 14 | 15 | [dev-packages] 16 | 17 | pytest = "*" 18 | twine = "*" 19 | "autopep8" = "*" 20 | 21 | 22 | [requires] 23 | 24 | python_version = "3.6" -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - libgeos-dev 9 | 10 | # command to install dependencies 11 | install: 12 | - pip install pipenv 13 | - pipenv install --dev 14 | 15 | # command to run the dependencies 16 | script: 17 | - pipenv run pytest 18 | - pipenv run autopep8 --in-place ./**/*.py 19 | - git diff --quiet 20 | -------------------------------------------------------------------------------- /examples/print_library.py: -------------------------------------------------------------------------------- 1 | from pycircuit.library import * 2 | 3 | 4 | def print_components(): 5 | for comp in Component.components: 6 | print(repr(comp)) 7 | 8 | 9 | def print_packages(): 10 | for package in Package.packages: 11 | print(repr(package)) 12 | 13 | 14 | def print_devices(): 15 | for device in Device.devices: 16 | print(repr(device)) 17 | 18 | 19 | if __name__ == '__main__': 20 | print_components() 21 | print_packages() 22 | print_devices() 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='pycircuit', 5 | packages=find_packages(exclude=['tests', 'examples']), 6 | version='0.0.2', 7 | description='Library for composing circuits and pcb layouts', 8 | long_description=open('README.md').read(), 9 | author='David Craven', 10 | author_email='david@craven.ch', 11 | url='https://github.com/dvc94ch/pycircuit', 12 | keywords=['eda', 'cad', 'hdl', 'kicad'], 13 | install_requires=[ 14 | 'numpy', 'scipy', 'shapely', 'pykicad', 'z3-solver' 15 | ], 16 | tests_require=['pytest'], 17 | license='ISC' 18 | ) 19 | -------------------------------------------------------------------------------- /examples/sallen_key/build.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.signal as sig 3 | from pycircuit.build import Builder 4 | from pycircuit.testbench import testbench 5 | from pycircuit.optimize import Optimizer 6 | from pycircuit.library.design_rules import oshpark_4layer 7 | 8 | from sallen_key import lp_sallen_key, top 9 | 10 | 11 | def lp_optimize(): 12 | spec = sig.butter(2, 2 * np.pi * 100, btype='low', analog=True) 13 | tb = Builder(testbench(lp_sallen_key())).compile() 14 | problem = Optimizer(tb, spec) 15 | cost = problem.optimize() 16 | print(cost) 17 | problem.plot_result() 18 | print(repr(problem.netlist)) 19 | 20 | 21 | if __name__ == '__main__': 22 | lp_optimize() 23 | #Builder(top(), oshpark_4layer).build() 24 | -------------------------------------------------------------------------------- /viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewer", 3 | "version": "0.0.1", 4 | "description": "Viewer for pycircuit generated files", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/dvc94ch/pycircuit.git" 12 | }, 13 | "keywords": [ 14 | "pycircuit", 15 | "svg", 16 | "graphviz" 17 | ], 18 | "author": "David Craven", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/dvc94ch/pycircuit/issues" 22 | }, 23 | "homepage": "https://github.com/dvc94ch/pycircuit#readme", 24 | "dependencies": { 25 | "express": "^4.16.2", 26 | "normalize.css": "^7.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/mcu/build.py: -------------------------------------------------------------------------------- 1 | from pycircuit.build import Builder 2 | from pycircuit.library.design_rules import oshpark_4layer 3 | from placer import Placer 4 | from pykicad.pcb import Zone 5 | from mcu import mcu 6 | 7 | 8 | def place(filein, fileout): 9 | placer = Placer() 10 | placer.place(filein, fileout) 11 | 12 | 13 | def post_process(pcb, kpcb): 14 | xmin, ymin, xmax, ymax = pcb.boundary() 15 | coords = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] 16 | 17 | zone = Zone(net_name='GND', layer='F.Cu', 18 | polygon=coords, clearance=0.3) 19 | 20 | kpcb.zones.append(zone) 21 | return kpcb 22 | 23 | 24 | if __name__ == '__main__': 25 | Builder(mcu(), oshpark_4layer, 26 | place=place, post_process=post_process).build() 27 | -------------------------------------------------------------------------------- /examples/common_emitter/build.py: -------------------------------------------------------------------------------- 1 | import ngspyce as ng 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | from pycircuit.formats import * 5 | from pycircuit.build import Builder 6 | from pycircuit.testbench import testbench 7 | 8 | from common_emitter import common_emitter_amplifer 9 | 10 | 11 | def sim(): 12 | tb = testbench(common_emitter_amplifer()) 13 | circuit = Builder(tb).compile() 14 | circuit.to_spice('common_emitter_amplifier.sp') 15 | ng.source('common_emitter_amplifier.sp') 16 | ng.cmd('tran 50us 10ms') 17 | 18 | print('\n'.join(ng.vector_names())) 19 | time, tp1 = map(ng.vector, ['time', 'V(VOUT)']) 20 | plt.plot(time, tp1, label='VOUT') 21 | plt.legend() 22 | plt.show() 23 | 24 | 25 | if __name__ == '__main__': 26 | sim() 27 | -------------------------------------------------------------------------------- /placer/grid.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | class Grid(object): 5 | def __init__(self, width, height): 6 | self.width = width 7 | self.height = height 8 | self.grid = [[' ' for i in range(width)] for i in range(height)] 9 | 10 | def add_box(self, box): 11 | h = int(box.height) 12 | w = int(box.width) 13 | b = int(box.bottom()) 14 | l = int(box.left()) 15 | for y in range(h): 16 | for x in range(w): 17 | self.grid[b + y][l + x] = box.symbol 18 | 19 | def __str__(self): 20 | result = '_' * (self.width + 2) + '\n' 21 | for line in reversed(self.grid): 22 | result += '|' 23 | for char in line: 24 | result += char 25 | result += '|\n' 26 | result += '_' * (self.width + 2) + '\n' 27 | return result 28 | -------------------------------------------------------------------------------- /examples/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | examplesdir = os.path.dirname(os.path.realpath(__file__)) 5 | examples = ['common_emitter', 'joule_thief', 'mcu', 'sallen_key'] 6 | 7 | 8 | def build_dir_path(example): 9 | return os.path.join(examplesdir, example, 'build') 10 | 11 | 12 | def file_path(example, file): 13 | return os.path.join(examplesdir, example, file) 14 | 15 | 16 | def build_file_path(example, file): 17 | return os.path.join(build_dir_path(example), file) 18 | 19 | 20 | def build(path): 21 | os.system('python3 %s' % path) 22 | 23 | 24 | def clean(path): 25 | os.system('rm -rf %s' % path) 26 | 27 | 28 | def open_svg(path): 29 | os.system('chromium %s &' % path) 30 | 31 | 32 | if __name__ == '__main__': 33 | clean('build') 34 | for example in examples: 35 | print('Building', example) 36 | build(file_path(example, example + '.py')) 37 | -------------------------------------------------------------------------------- /pycircuit/formats/json.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import Circuit, Netlist 2 | from pycircuit.pcb import Pcb 3 | from pycircuit.formats import extends, _to_file, _to_json, _from_file 4 | 5 | 6 | @extends(Circuit) 7 | def to_file(self, path): 8 | _to_file(path, _to_json(self.to_object())) 9 | 10 | 11 | @staticmethod 12 | @extends(Circuit) 13 | def from_file(path): 14 | return Circuit.from_object(_from_file(path)) 15 | 16 | 17 | @extends(Netlist) 18 | def to_file(self, path): 19 | _to_file(path, _to_json(self.to_object())) 20 | 21 | 22 | @staticmethod 23 | @extends(Netlist) 24 | def from_file(path): 25 | return Netlist.from_object(_from_file(path)) 26 | 27 | 28 | @extends(Pcb) 29 | def to_file(self, path): 30 | _to_file(path, _to_json(self.to_object())) 31 | 32 | 33 | @staticmethod 34 | @extends(Pcb) 35 | def from_file(path): 36 | return Pcb.from_object(_from_file(path)) 37 | -------------------------------------------------------------------------------- /pycircuit/library/devices.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from pycircuit.device import * 3 | 4 | for device in ['R', 'C']: 5 | for package in ['0805']: 6 | Device('%s%s' % (device, package), device, package, 7 | Map('1', 'A'), 8 | Map('2', 'B')) 9 | 10 | for package in ['0805']: 11 | Device('D%s' % package, 'D', package, 12 | Map('1', '+'), 13 | Map('2', '-')) 14 | 15 | for a, b, c in itertools.permutations('BCE', 3): 16 | Device('SOT23' + a + b + c, 'Q', 'SOT23', 17 | Map('1', a), 18 | Map('2', b), 19 | Map('3', c), 20 | Map(None, 'SUBSTRATE')) 21 | 22 | for a, b, c in itertools.permutations('GSD'): 23 | Device('SOT23' + a + b + c, 'M', 'SOT23', 24 | Map('1', a), 25 | Map('2', b), 26 | Map('3', c), 27 | Map(None, 'SUBSTRATE')) 28 | 29 | Device('TP', 'TP', 'Pins_1x1', Map('A1', 'TP')) 30 | -------------------------------------------------------------------------------- /pycircuit/testbench.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import * 2 | 3 | 4 | def testbench(circuit): 5 | tb = Circuit(circuit.name + '_testbench') 6 | Circuit.active_circuit = tb 7 | 8 | dut = SubInst(circuit, _parent=tb) 9 | gnd = Net('gnd', _parent=tb) 10 | nets = [] 11 | 12 | for port in dut.circuit.external_ports(): 13 | if port.type == PortType.GND: 14 | dut[port.name] = gnd 15 | else: 16 | nets.append(Net(port.name, _parent=tb)) 17 | dut[port.name] = nets[-1] 18 | 19 | if port.type == PortType.POWER: 20 | Inst('V', 'dc %d' % parse_power_net(port.name), 21 | _parent=tb)['+', '-'] = nets[-1], gnd 22 | elif port.type == PortType.IN: 23 | Inst('V', 'sin(0, 0.1, 1K) ac 1', _parent=tb)[ 24 | '+', '-'] = nets[-1], gnd 25 | elif port.type == PortType.OUT: 26 | Inst('TP', _parent=tb)['TP'] = nets[-1] 27 | 28 | return tb 29 | -------------------------------------------------------------------------------- /examples/common_emitter/common_emitter.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import * 2 | from pycircuit.library import * 3 | 4 | 5 | @circuit('Common Emitter', 'gnd', '12V', 'vin', 'vout') 6 | def common_emitter_amplifer(self, gnd, vcc, vin, vout): 7 | nb, ne = nets('nb ne') 8 | Inst('Q', 'npn sot23')['B', 'C', 'E'] = nb, vout, ne 9 | 10 | # Current limiting resistor 11 | Inst('R', '1.2k')['~', '~'] = vcc, vout 12 | 13 | # Thermal stabilization (leads to a gain reduction) 14 | Inst('R', '220')['~', '~'] = ne, gnd 15 | # Shorts Re for AC signal (increases gain) 16 | Inst('C', '10uF')['~', '~'] = ne, gnd 17 | 18 | # Biasing resistors 19 | Inst('R', '20k')['~', '~'] = vcc, nb 20 | Inst('R', '3.6k')['~', '~'] = nb, gnd 21 | # Decoupling capacitor 22 | Inst('C', '10uF')['~', '~'] = vin, nb 23 | 24 | 25 | if __name__ == '__main__': 26 | from pycircuit.formats import * 27 | from pycircuit.build import Builder 28 | 29 | Builder(common_emitter_amplifer()).compile() 30 | -------------------------------------------------------------------------------- /pycircuit/library/old/connectors.py: -------------------------------------------------------------------------------- 1 | from pycircuit.device import * 2 | 3 | 4 | # Connectors 5 | # Power Connectors 6 | # DC Connector 7 | Device('DCCONN', pins=[ 8 | Pwr('GND'), 9 | PwrOut('V') 10 | ]) 11 | 12 | 13 | # Data Connectors 14 | # USB Connector 15 | Device('USBCONN', pins=[ 16 | Pwr('GND'), 17 | PwrOut('VBUS'), 18 | Pin('D+', Fun('USB2', 'D+')), 19 | Pin('D-', Fun('USB2', 'D-')) 20 | ]) 21 | 22 | # USB-C Connector 23 | Device('USBCCONN', pins=[ 24 | Pwr('GND'), 25 | PwrOut('VBUS'), 26 | Pin('TX1+', Fun('USB3', 'TX1+')), 27 | Pin('TX1-', Fun('USB3', 'TX1-')), 28 | Pin('RX1+', Fun('USB3', 'RX1+')), 29 | Pin('RX1-', Fun('USB3', 'RX1-')), 30 | Pin('TX2+', Fun('USB3', 'TX2+')), 31 | Pin('TX2-', Fun('USB3', 'TX2-')), 32 | Pin('RX2+', Fun('USB3', 'RX2+')), 33 | Pin('RX2-', Fun('USB3', 'RX2-')), 34 | Pin('D+', Fun('USB2', 'D+')), 35 | Pin('D-', Fun('USB2', 'D-')), 36 | Pin('CC1'), 37 | Pin('CC2'), 38 | Pin('SBU1'), 39 | Pin('SBU2') 40 | ]) 41 | -------------------------------------------------------------------------------- /viewer/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var path = require('path'); 4 | 5 | function random_port() { 6 | return Math.floor(Math.random() * 100) + 3000; 7 | } 8 | 9 | var index = path.join(__dirname, 'index.html'); 10 | var port = process.argv[2] || random_port(); 11 | var net = path.join(process.cwd(), process.argv[3] || path.join('build', 'net.dot.svg')); 12 | var pcb = path.join(process.cwd(), process.argv[4] || path.join('build', 'pcb.svg')); 13 | 14 | app.use('/css', express.static(path.join(__dirname, 'css'))); 15 | app.use('/js', express.static(path.join(__dirname, 'js'))); 16 | 17 | app.get('/', function(req, res) { 18 | res.sendFile(index); 19 | }); 20 | 21 | app.get('/net', function(req, res) { 22 | res.sendFile(net); 23 | }); 24 | 25 | app.get('/pcb', function(req, res) { 26 | res.sendFile(pcb); 27 | }); 28 | 29 | app.listen(port, function () { 30 | console.log('Viewer listening on port', port); 31 | console.log('net file:', net); 32 | console.log('pcb file:', pcb); 33 | }); 34 | -------------------------------------------------------------------------------- /pycircuit/formats/__init__.py: -------------------------------------------------------------------------------- 1 | import json as j 2 | import os 3 | import sys 4 | 5 | 6 | def extends(klass): 7 | def decorator(func): 8 | setattr(klass, func.__name__, func) 9 | return func 10 | return decorator 11 | 12 | 13 | def _to_json(obj): 14 | return j.dumps(obj, sort_keys=True, 15 | indent=2, separators=(',', ': ')) 16 | 17 | 18 | def _to_file(path, string): 19 | with open(path, 'w+') as f: 20 | f.write(string) 21 | 22 | 23 | def _from_file(path): 24 | with open(path) as f: 25 | return j.loads(f.read()) 26 | 27 | 28 | def export(string, filename, extension): 29 | if filename is None: 30 | filename = os.path.basename(sys.argv[0]) 31 | filename = os.path.splitext(filename)[0] 32 | 33 | if not filename.endswith(extension): 34 | filename += extension 35 | 36 | with open(filename, 'w+') as f: 37 | f.write(string) 38 | 39 | 40 | def polygon_to_lines(coords): 41 | coords.append(coords[0]) 42 | for i in range(len(coords) - 1): 43 | yield coords[i], coords[i + 1] 44 | 45 | 46 | __all__ = ['json', 'svg', 'yosys', 'spice', 'kicad'] 47 | -------------------------------------------------------------------------------- /viewer/css/pcb.svg.css: -------------------------------------------------------------------------------- 1 | text { 2 | font-family: 'Verdana'; 3 | } 4 | 5 | .outline { 6 | fill-opacity: 0; 7 | stroke: black; 8 | stroke-width: 0.05; 9 | } 10 | 11 | .crtyd { 12 | stroke: black; 13 | stroke-width: 0.05; 14 | fill-opacity: 0; 15 | } 16 | 17 | .pad { 18 | fill-opacity: 0.5; 19 | } 20 | 21 | .ref { 22 | font-size: 0.8px; 23 | } 24 | 25 | .pad-label { 26 | font-size: 0.4px; 27 | fill: white; 28 | } 29 | 30 | .via { 31 | fill-opacity: 1 32 | } 33 | 34 | .via .dia { 35 | fill: black; 36 | } 37 | 38 | .via .drill { 39 | fill: white; 40 | } 41 | 42 | .seg { 43 | stroke-linecap: round; 44 | stroke-opacity: 0.5; 45 | } 46 | 47 | /* Color segments and pads */ 48 | .layer.top .pad { 49 | fill: red; 50 | } 51 | 52 | .layer.top .seg { 53 | stroke: red; 54 | } 55 | 56 | .layer.inner1 .pad { 57 | fill: blue; 58 | } 59 | 60 | .layer.inner1 .seg { 61 | stroke: blue; 62 | } 63 | 64 | .layer.inner2 .pad { 65 | fill: purple; 66 | } 67 | 68 | .layer.inner2 .seg { 69 | stroke: purple; 70 | } 71 | 72 | .layer.bottom .pad { 73 | fill: green; 74 | } 75 | 76 | .layer.bottom .seg { 77 | stroke: green; 78 | } 79 | -------------------------------------------------------------------------------- /examples/joule_thief/joule_thief.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import * 2 | from pycircuit.library import * 3 | 4 | 5 | Device('V0805', 'V', '0805', 6 | Map('1', '+'), 7 | Map('2', '-')) 8 | 9 | Package('TDK ACT45B', RectCrtyd(5.9, 3.4), DualPads(4, 2.5, radius=2.275), 10 | package_size=(5.9, 3.4), pad_size=(0.9, 1.35)) 11 | 12 | Device('TDK ACT45B', 'Transformer_1P_1S', 'TDK ACT45B', 13 | Map('1', 'L1.1'), Map('2', 'L2.1'), Map('3', 'L2.2'), Map('4', 'L1.2')) 14 | 15 | 16 | @circuit('Joule Thief', 'gnd', 'vcc', None, 'vout') 17 | def joule_thief(self, gnd, vin, vout): 18 | nb, nr = nets('nb nr') 19 | 20 | with Inst('Transformer_1P_1S', '1mH') as tr1: 21 | tr1['L1.1', 'L1.2'] = nr, nb 22 | tr1['L2.2', 'L2.1'] = vin, vout 23 | 24 | Inst('R', '1k 0805')['~', '~'] = vin, nr 25 | Inst('Q', 'npn sot23')['B', 'C', 'E'] = nb, vout, gnd 26 | Inst('D', 'led white 0805')['+', '-'] = vout, gnd 27 | 28 | 29 | @circuit('Top') 30 | def top(self): 31 | vcc, gnd, vout = nets('vcc gnd vout') 32 | 33 | SubInst(joule_thief())['vcc', 'vout', 'gnd'] = vcc, vout, gnd 34 | Inst('V', '1.5V')['+', '-'] = vcc, gnd 35 | Inst('TP')['TP'] = vout 36 | 37 | 38 | if __name__ == '__main__': 39 | from pycircuit.formats import * 40 | from pycircuit.build import Builder 41 | 42 | Builder(joule_thief()).compile() 43 | Builder(top()).compile() 44 | -------------------------------------------------------------------------------- /examples/voltage_divider/voltage_divider.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import * 2 | from pycircuit.device import * 3 | from pycircuit.library import * 4 | from pycircuit.build import Builder 5 | from placer.place import Placer 6 | from router.router import Router 7 | 8 | 9 | Component('Connector', 'A connector', Pin('vin'), Pin('vout'), Pin('gnd')) 10 | Device('Connector', 'Connector', 'Pins_3x1', 11 | Map('A1', 'vin'), 12 | Map('A2', 'vout'), 13 | Map('A3', 'gnd') 14 | ) 15 | 16 | 17 | @circuit('Voltage Divider', 'gnd', None, 'vin', 'vout') 18 | def voltage_divider(self, gnd, vin, vout): 19 | Inst('R', '10k 0805')['~', '~'] = vin, vout 20 | Inst('R', '10k 0805')['~', '~'] = vout, gnd 21 | 22 | 23 | @circuit('Top') 24 | def top(self): 25 | vin, vout, gnd = nets('vin vout gnd') 26 | SubInst(voltage_divider())['vin', 'vout', 'gnd'] = vin, vout, gnd 27 | Inst('Connector', 'Pins_3x1')['vin', 'vout', 'gnd'] = vin, vout, gnd 28 | 29 | 30 | def place(fin, fout): 31 | p = Placer() 32 | p.place(fin, fout) 33 | 34 | 35 | def route(fin, fout): 36 | r = Router(grid_size=.5, maxflow_enforcement_level=3) 37 | r.route(fin, fout) 38 | 39 | 40 | if __name__ == '__main__': 41 | outline = rectangle_with_mounting_holes( 42 | 20, 10, inset=1, hole_shift=2, hole_dia=1) 43 | 44 | Builder(top(), outline=outline, 45 | pcb_attributes=oshpark_2layer(), 46 | place=place, route=route).build() 47 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pycircuit.circuit import * 3 | 4 | 5 | class ParserTests(unittest.TestCase): 6 | def setUp(self): 7 | self.circuit = Circuit('ParserTests') 8 | Circuit.active_circuit = self.circuit 9 | 10 | def test_parse_busses(self): 11 | ns = nets('') 12 | assert ns is None 13 | 14 | n0 = nets('n0') 15 | assert n0.name == 'n0' 16 | 17 | n1, n2, n3 = nets('n1 n2 n3') 18 | assert n1.name == 'n1' 19 | assert n2.name == 'n2' 20 | assert n3.name == 'n3' 21 | assert not n1.guid == n2.guid 22 | assert not n2.guid == n3.guid 23 | assert not n1.guid == n3.guid 24 | 25 | uart = nets('uart[2]') 26 | assert len(uart) == 2 27 | assert uart[0].name == 'uart[0]' 28 | assert uart[1].name == 'uart[1]' 29 | assert uart[0].guid == uart[1].guid 30 | 31 | gpio, uart = nets('gpio[3] usart[2]') 32 | assert len(gpio) == 3 33 | assert len(uart) == 2 34 | assert not gpio[0].guid == uart[0].guid 35 | 36 | def test_parse_power_net(self): 37 | assert parse_power_net('GND') == 0 38 | assert parse_power_net('gnd') == 0 39 | 40 | assert parse_power_net('0') == 0 41 | assert parse_power_net('0V') == 0 42 | assert parse_power_net('V0') == 0 43 | 44 | assert parse_power_net('3.3') == 3.3 45 | assert parse_power_net('3.3V') == 3.3 46 | assert parse_power_net('V3.3') == 3.3 47 | assert parse_power_net('3V3') == 3.3 48 | -------------------------------------------------------------------------------- /examples/joule_thief/build.py: -------------------------------------------------------------------------------- 1 | import joule_thief 2 | from pycircuit.build import Builder 3 | from pycircuit.library.design_rules import oshpark_4layer 4 | from pycircuit.library.outlines import sick_of_beige 5 | from placer import Placer 6 | from router import Router 7 | from pykicad.pcb import Zone 8 | 9 | 10 | ''' 11 | def sim(): 12 | import ngspyce as ng 13 | import numpy as np 14 | from matplotlib import pyplot as plt 15 | netlist = Builder(top()).get_netlist() 16 | netlist.to_spice('joule_thief.sp') 17 | ng.source('joule_thief.sp') 18 | ng.cmd('tran 1us 500us') 19 | 20 | print('\n'.join(ng.vector_names())) 21 | time, tp1 = map(ng.vector, ['time', 'V(VOUT)']) 22 | plt.plot(time, tp1, label='VOUT') 23 | plt.legend() 24 | plt.show() 25 | ''' 26 | 27 | 28 | def place(filein, fileout): 29 | placer = Placer() 30 | placer.place(filein, fileout) 31 | 32 | 33 | def route(filein, fileout): 34 | router = Router(maxflow_enforcement_level=2) 35 | router.route(filein, fileout) 36 | 37 | 38 | def post_process(pcb, kpcb): 39 | coords = list(pcb.outline.polygon.interiors[0].coords) 40 | 41 | zone = Zone(net_name='gnd', layer='F.Cu', 42 | polygon=coords, clearance=0.3) 43 | 44 | kpcb.zones.append(zone) 45 | return kpcb 46 | 47 | 48 | if __name__ == '__main__': 49 | pcb_attributes = oshpark_4layer() 50 | # Only allow placement on top layer 51 | pcb_attributes.layers.placement_layers.pop() 52 | 53 | Builder(joule_thief.top(), outline=sick_of_beige('DP5031'), 54 | pcb_attributes=pcb_attributes, place=place, 55 | # route=route, 56 | post_process=post_process).build() 57 | -------------------------------------------------------------------------------- /placer/bin.py: -------------------------------------------------------------------------------- 1 | from z3 import * 2 | 3 | 4 | class Bin(object): 5 | counter = 0 6 | 7 | def __init__(self, width, height): 8 | self.id = Bin.counter 9 | Bin.counter += 1 10 | 11 | self.width = width 12 | self.height = height 13 | 14 | def dim(self): 15 | return (self.width, self.height) 16 | 17 | def center(self): 18 | return (self.width / 2, self.height / 2) 19 | 20 | def area(self): 21 | return self.width * self.height 22 | 23 | def __str__(self): 24 | return 'width=%s height=%s area=%s' % (self.width, self.height, 25 | self.area()) 26 | 27 | 28 | class Z3Bin(Bin): 29 | def __init__(self): 30 | super().__init__(0, 0) 31 | self.var_width = Int('bin%s_w' % str(self.id)) 32 | self.var_height = Int('bin%s_h' % str(self.id)) 33 | self.var_area = Int('bin%s_a' % str(self.id)) 34 | self.var_center_x = Int('bin%s_cx' % str(self.id)) 35 | self.var_center_y = Int('bin%s_cy' % str(self.id)) 36 | 37 | def var_center(self): 38 | return (self.var_center_x, self.var_center_y) 39 | 40 | def range_constraint(self): 41 | return And(self.var_width >= 0, 42 | self.var_height >= 0, 43 | self.var_width == 2 * self.var_center_x, 44 | self.var_height == 2 * self.var_center_y) 45 | 46 | def area_constraint(self, area): 47 | return And(self.var_area == self.var_width * self.var_height, 48 | self.var_area <= area) 49 | 50 | def eval(self, model): 51 | self.width = model[self.var_width].as_long() 52 | self.height = model[self.var_height].as_long() 53 | -------------------------------------------------------------------------------- /pycircuit/library/packages.py: -------------------------------------------------------------------------------- 1 | from pycircuit.package import * 2 | 3 | 4 | # Basic 5 | for width in range(1, 3): 6 | for length in range(1, 21): 7 | t_length, t_width = 2.41 + (length - 1) * 2.54, 2.41 8 | Package('Pins_%dx%d' % (length, width), RectCrtyd(t_length, t_width), 9 | GridPads(width, length, pitch=2.54), 10 | package_size=(t_length, t_width), 11 | pad_size=(2.41, 2.41), pad_drill=.51, pad_shape='circle') 12 | 13 | Package('0805', IPCGrid(4, 8), TwoPads(1.9), 14 | package_size=(1.4, 2.15), pad_size=(1.5, 1.3)) 15 | 16 | Package('SOT23', IPCGrid(8, 8), Sot23Pads(2.2, 0.95), 17 | package_size=(3, 1.4), pad_size=(1, 1.4)) 18 | 19 | 20 | # DIP 21 | Package('DIP8', RectCrtyd(9.7, 12.55), DualPads(8, pitch=2.54, radius=3.81), 22 | package_size=(7.35, 12.21), pad_size=(1.6, 1.6), 23 | pad_shape='circle', pad_drill=0.8) 24 | 25 | 26 | # QFN 27 | Package('QFN16', RectCrtyd(5.3, 5.3), QuadPads(16, pitch=0.65, radius=2, thermal_pad=2.5), 28 | package_size=(5, 5), pad_size=(0.35, 0.8)) 29 | 30 | Package('QFN48', RectCrtyd(7.5, 7.5), QuadPads(48, pitch=0.5, radius=3.5, thermal_pad=5.1), 31 | package_size=(7, 7), pad_size=(0.28, 0.724)) # FIXME: Radius is only approximate!! 32 | 33 | Package('QFN64', RectCrtyd(9, 9), QuadPads(64, 0.5, 4.5, thermal_pad=6), 34 | package_size=(9, 9), pad_size=(0.28, 0.724)) # FIXME: Radius is only approximate!! 35 | 36 | 37 | # TQFP 38 | # FIXME: Radius is only approximate!! 39 | Package('TQFP144', RectCrtyd(20, 20), QuadPads(144, pitch=0.5, radius=10)) 40 | 41 | 42 | # BGA 43 | Package('PBGA16_8x8', RectCrtyd(8, 8), GridPads(4, 4, pitch=1.5), 44 | package_size=(8, 8), pad_size=(.6, .6), pad_shape='circle') 45 | -------------------------------------------------------------------------------- /pycircuit/library/outlines.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from shapely.geometry.polygon import Polygon 3 | from pycircuit.outline import Hole, Outline 4 | 5 | 6 | def rectangle_with_mounting_holes(width, height, inset, hole_shift, hole_drill): 7 | shift_matrix = np.array([[1, 1], [-1, 1], [-1, -1], [1, -1]]) 8 | 9 | exterior_coords = np.array( 10 | [[0, 0], [width, 0], [width, height], [0, height]]) 11 | interior_coords = exterior_coords + shift_matrix * inset 12 | interior = Polygon(interior_coords) 13 | 14 | hole_coords = exterior_coords + shift_matrix * hole_shift 15 | holes = [] 16 | for pos in hole_coords: 17 | holes.append(Hole(pos, hole_drill)) 18 | hole_clearance = hole_drill / 2 + inset 19 | interior = interior.difference( 20 | Polygon(shift_matrix * hole_clearance + pos)) 21 | 22 | outline = Polygon(exterior_coords, [interior.exterior.coords]) 23 | return Outline(outline, *holes) 24 | 25 | 26 | def sick_of_beige(name): 27 | '''http://dangerousprototypes.com/docs/Sick_of_Beige_standard_PCB_sizes_v1.0''' 28 | 29 | lookup = { 30 | 'DP5031': (50, 31), 31 | 'DP6037': (60, 37), 32 | 'DP7043': (70, 43), 33 | 'DP8049': (80, 49), 34 | 'DP9056': (90, 56), 35 | 'DP10062': (100, 62), 36 | 'DP3030': (30, 30), 37 | 'DP4040': (40, 40), 38 | 'DP5050': (50, 50), 39 | 'DP6060': (60, 60), 40 | 'DP7070': (70, 70), 41 | 'DP8080': (80, 80), 42 | } 43 | 44 | width, height = lookup[name] 45 | inset = 1.7 46 | hole_shift = 4 47 | hole_dia = 3.2 48 | 49 | return rectangle_with_mounting_holes(width=width, height=height, inset=inset, 50 | hole_shift=hole_shift, hole_dia=hole_dia) 51 | -------------------------------------------------------------------------------- /viewer/css/graphviz.svg.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Mountainstorm 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | #graph { 24 | background-color: #2e3e56; 25 | } 26 | 27 | /* this element needs tooltip positioning to work */ 28 | .graphviz-svg { 29 | position: relative; 30 | } 31 | 32 | /* stop tooltips wrapping */ 33 | .graphviz-svg .tooltip-inner { 34 | white-space: nowrap; 35 | } 36 | 37 | /* stop people selecting text on nodes */ 38 | .graphviz-svg text { 39 | -webkit-touch-callout: none; 40 | -webkit-user-select: none; 41 | -khtml-user-select: none; 42 | -moz-user-select: none; 43 | -ms-user-select: none; 44 | user-select: none; 45 | cursor: default; 46 | } 47 | -------------------------------------------------------------------------------- /examples/sallen_key/sallen_key.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import * 2 | from pycircuit.library import * 3 | 4 | 5 | def sallen_key(z1, z2, z3, z4, vcc, vee): 6 | 7 | @circuit('Sallen Key', 'gnd', '%s %s' % (vcc, vee), 'vin', 'vout') 8 | def sallen_key_topology(self, gnd, vcc, vee, vin, vout): 9 | n1, n2 = nets('n1 n2') 10 | 11 | Inst(z1)['~', '~'] = vin, n1 12 | Inst(z2)['~', '~'] = n1, n2 13 | Inst(z3)['~', '~'] = n1, vout 14 | Inst(z4)['~', '~'] = n2, gnd 15 | 16 | with Inst('OP', 'OP') as op: 17 | op['+', '-', 'OUT'] = n2, vout, vout 18 | op['VCC', 'VEE'] = vcc, vee 19 | 20 | return sallen_key_topology() 21 | 22 | 23 | def lp_sallen_key(vcc='+12V', vee='-12V'): 24 | return sallen_key('R', 'R', 'C', 'C', vcc, vee) 25 | 26 | 27 | Device('V0805', 'V', '0805', 28 | Map('1', '+'), 29 | Map('2', '-')) 30 | 31 | Device('OPDIP', 'OP', 'DIP8', 32 | Map('1', 'VCC'), 33 | Map('2', 'VEE'), 34 | Map('3', '+'), 35 | Map('4', 'OUT'), 36 | Map('5', '-'), 37 | Map('6', None), 38 | Map('7', None), 39 | Map('8', None)) 40 | 41 | 42 | @circuit('Sallen Key Top', 'gnd') 43 | def top(self, gnd): 44 | vcc, vee, vin, vout = nets('+12V -12V vin vout') 45 | # VCC 46 | Inst('V')['+', '-'] = vcc, gnd 47 | # VEE 48 | Inst('V')['+', '-'] = gnd, vee 49 | # Vin 50 | Inst('V')['+', '-'] = vin, gnd 51 | 52 | Inst('TP')['TP'] = vout 53 | 54 | with SubInst(lp_sallen_key()) as sk: 55 | sk['+12V', '-12V', 'gnd', 'vin', 'vout'] = vcc, vee, gnd, vin, vout 56 | 57 | 58 | if __name__ == '__main__': 59 | from pycircuit.formats import * 60 | from pycircuit.build import Builder 61 | 62 | Builder(lp_sallen_key()).compile() 63 | Builder(top()).compile() 64 | -------------------------------------------------------------------------------- /pycircuit/library/old/busses.py: -------------------------------------------------------------------------------- 1 | from pycircuit.device import * 2 | 3 | 4 | Bus('OP', 5 | Fun('+', Fun.INPUT), 6 | Fun('-', Fun.INPUT), 7 | Fun('OUT', Fun.OUTPUT)) 8 | 9 | Bus('PWM', 10 | Fun('0', Fun.OUTPUT), 11 | Fun('1', Fun.OUTPUT), 12 | Fun('2', Fun.OUTPUT), 13 | Fun('3', Fun.OUTPUT)) 14 | 15 | Bus('UART', 16 | Fun('TX', Fun.OUTPUT), 17 | Fun('RX', Fun.INPUT)) 18 | 19 | Bus('JTAG:M', 20 | Fun('TCK', Fun.OUTPUT), 21 | Fun('TDO', Fun.INPUT), 22 | Fun('TMS', Fun.OUTPUT), 23 | Fun('TDI', Fun.OUTPUT)) 24 | 25 | Bus('JTAG:S', 26 | Fun('TCK', Fun.INPUT), 27 | Fun('TDO', Fun.OUTPUT), 28 | Fun('TMS', Fun.INPUT), 29 | Fun('TDI', Fun.INPUT)) 30 | 31 | Bus('I2C:M', 32 | Fun('SDA', Fun.BIDIR), 33 | Fun('SCL', Fun.OUTPUT)) 34 | 35 | Bus('I2C:S', 36 | Fun('SDA', Fun.BIDIR), 37 | Fun('SCL', Fun.INPUT)) 38 | 39 | Bus('SPI:M', 40 | Fun('SCLK', Fun.OUTPUT), 41 | Fun('MOSI', Fun.OUTPUT), 42 | Fun('MISO', Fun.INPUT), 43 | Fun('SS0', Fun.OUTPUT), 44 | Fun('SS1', Fun.OUTPUT), 45 | Fun('SS2', Fun.OUTPUT), 46 | Fun('SS3', Fun.OUTPUT)) 47 | 48 | Bus('SPI:S', 49 | Fun('SCLK', Fun.INPUT), 50 | Fun('MOSI', Fun.INPUT), 51 | Fun('MISO', Fun.OUTPUT), 52 | Fun('SS', Fun.INPUT)) 53 | 54 | Bus('QSPI:M', 55 | Fun('DQ3', Fun.BIDIR), 56 | Fun('DQ2', Fun.BIDIR), 57 | Fun('DQ1', Fun.BIDIR), 58 | Fun('DQ0', Fun.BIDIR), 59 | Fun('SCLK', Fun.OUTPUT), 60 | Fun('SS0', Fun.OUTPUT), 61 | Fun('SS1', Fun.OUTPUT), 62 | Fun('SS2', Fun.OUTPUT), 63 | Fun('SS3', Fun.OUTPUT)) 64 | 65 | Bus('QSPI:S', 66 | Fun('DQ3', Fun.BIDIR), 67 | Fun('DQ2', Fun.BIDIR), 68 | Fun('DQ1', Fun.BIDIR), 69 | Fun('DQ0', Fun.BIDIR), 70 | Fun('SCLK', Fun.INPUT), 71 | Fun('SS', Fun.INPUT)) 72 | 73 | Bus('USB2', 74 | Fun('D+', Fun.BIDIR), 75 | Fun('D-', Fun.BIDIR)) 76 | 77 | Bus('USB3', 78 | Fun('TX1+', Fun.OUTPUT), 79 | Fun('TX1-', Fun.OUTPUT), 80 | Fun('RX1+', Fun.INPUT), 81 | Fun('RX1-', Fun.INPUT), 82 | Fun('TX2+', Fun.OUTPUT), 83 | Fun('TX2-', Fun.OUTPUT), 84 | Fun('RX2+', Fun.INPUT), 85 | Fun('RX2-', Fun.INPUT)) 86 | -------------------------------------------------------------------------------- /pycircuit/library/old/circuits.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import * 2 | 3 | 4 | @circuit('Pierce oscillator') 5 | def pierce_oscillator(freq, cap, res=False): 6 | Node('Y', 'XTAL', freq) 7 | Node('C1', 'C', cap) 8 | Node('C2', 'C', cap) 9 | 10 | xi = Net('XTAL_XI') + Ref('Y')['~'] + Ref('C1')['~'] 11 | xo = Net('XTAL_XO') + Ref('Y')['~'] + Ref('C2')['~'] 12 | Net('GND') + Ref('C1')['~'] + Ref('C2')['~'] 13 | 14 | if res: 15 | Node('R', 'R', res) 16 | xi + Ref('R')['~'] 17 | xo + Ref('R')['~'] 18 | 19 | 20 | @circuit('Level shifter') 21 | def level_shifter(r_low=False, r_high='10K'): 22 | Node('R_HIGH', 'R', r_high) 23 | Node('Q', 'M') 24 | 25 | vdd_low = Net('VDD_LOW') + Ref('Q')['G'] 26 | low = Net('LOW') + Ref('Q')['S'] 27 | 28 | if r_low: 29 | Node('R_LOW', 'R', r_low) 30 | vdd_low + Ref('R_LOW')['~'] 31 | low + Ref('R_LOW')['~'] 32 | 33 | Net('VDD_HIGH') + Ref('R_HIGH')['~'] 34 | Net('HIGH') + Ref('Q')['D'] + Ref('R_HIGH')['~'] 35 | 36 | 37 | @circuit('Button') 38 | def button(pullup='100K', esd_protection=True): 39 | Node('BTN', 'BTN') 40 | Node('PULLUP', 'R', pullup) 41 | Net('VDD') + Ref('PULLUP')['~'] 42 | inp = Net('IN') + Ref('PULLUP')['~'] + Ref('BTN')['~'] 43 | gnd = Net('GND') + Ref('BTN')['~'] 44 | 45 | if esd_protection: 46 | Node('TVS', 'DD') 47 | inp + Ref('TVS')['~'] 48 | gnd + Ref('TVS')['~'] 49 | 50 | 51 | @circuit('Decoupling') 52 | def decoupling_capacitors(*args): 53 | vdd, vss = Net('VDD'), Net('VSS') 54 | for i, c in enumerate(args): 55 | name = 'C' + str(i + 1) 56 | Node(name, 'C', c) 57 | vdd + Ref(name)['~'] 58 | vss + Ref(name)['~'] 59 | 60 | #@circuit('Debouncer') 61 | # def debouncer(r1='470', r2='10K', c='10nF', diode=True): 62 | # Node('R1', 'R', r1) 63 | # Node('R2', 'R', r2) 64 | # Node('C', 'C', c) 65 | 66 | 67 | @circuit('RGB') 68 | def rgb(r='330'): 69 | Node('RGB', 'RGB_A') 70 | 71 | def led(color): 72 | Node('R' + color[0], 'R', r) 73 | Net(color) + Ref('R' + color[0])['~'] 74 | Net() + Ref('R' + color[0])['~'] + Ref('RGB')[color[0]] 75 | 76 | led('RED') 77 | led('GREEN') 78 | led('BLUE') 79 | Net('VDD') + Ref('RGB')['A'] 80 | -------------------------------------------------------------------------------- /viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 |

Click node to highlight; Shift-scroll to zoom; Esc to unhighlight

20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/mcu/mcu.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import * 2 | from pycircuit.library import * 3 | 4 | 5 | Component('MCU', 'Microcontroller', 6 | Gnd('GND'), 7 | Pwr('5V'), 8 | In('XTAL_XI'), 9 | Out('XTAL_XO'), 10 | In('JTAG_TCK'), 11 | In('JTAG_TMS'), 12 | In('JTAG_TDI'), 13 | Out('JTAG_TDO'), 14 | Io('GPIO_1', BusFun('UART0', 'UART_TX')), 15 | Io('GPIO_2', BusFun('UART0', 'UART_RX')), 16 | Io('GPIO_3'), 17 | Io('GPIO_4'), 18 | Io('GPIO_5', BusFun('UART1', 'UART_TX')), 19 | Io('GPIO_6', BusFun('UART1', 'UART_RX')), 20 | Io('GPIO_7')) 21 | 22 | Device('MCUQFN16', 'MCU', 'QFN16', 23 | Map('1', 'GPIO_1'), 24 | Map('2', 'GPIO_2'), 25 | Map('3', 'GPIO_3'), 26 | Map('4', 'GPIO_4'), 27 | Map('5', '5V'), 28 | Map('6', 'GND'), 29 | Map('7', 'GPIO_5'), 30 | Map('8', 'GPIO_6'), 31 | Map('9', 'XTAL_XI'), 32 | Map('10', 'XTAL_XO'), 33 | Map('11', 'GPIO_7'), 34 | Map('12', 'JTAG_TCK'), 35 | Map('13', 'JTAG_TDO'), 36 | Map('14', 'JTAG_TMS'), 37 | Map('15', 'JTAG_TDI'), 38 | Map('16', None), 39 | Map('17', 'GND')) 40 | 41 | Device('V0805', 'V', '0805', 42 | Map('1', '+'), 43 | Map('2', '-')) 44 | 45 | Device('OSC0805', 'XTAL', '0805', 46 | Map('1', 'A'), 47 | Map('2', 'B')) 48 | 49 | 50 | @circuit('LED', 'gnd', None, 'vin') 51 | def led(self, gnd, vin): 52 | n1 = nets('n1') 53 | 54 | Inst('R')['~', '~'] = vin, n1 55 | Inst('D', 'led')['+', '-'] = n1, gnd 56 | 57 | 58 | @circuit('RGB', 'gnd', None, 'red green blue') 59 | def rgb(self, gnd, *inputs): 60 | for port in inputs: 61 | SubInst(led())['vin', 'gnd'] = port, gnd 62 | 63 | 64 | @circuit('MCU') 65 | def mcu(self): 66 | vcc, gnd, clk, gpio, uart = nets('5V GND clk[2] gpio[3] uart[2]') 67 | 68 | Inst('V')['+', '-'] = vcc, gnd 69 | Inst('XTAL')['~', '~'] = clk 70 | SubInst(rgb())['red', 'green', 'blue', 'gnd'] = *gpio, gnd 71 | 72 | with Inst('MCU') as mcu: 73 | mcu['5V', 'GND'] = vcc, gnd 74 | mcu['XTAL_XI', 'XTAL_XO'] = clk 75 | mcu['GPIO', 'GPIO', 'GPIO'] = gpio 76 | mcu['UART_TX', 'UART_RX'] = uart 77 | mcu['UART_RX', 'UART_TX'] = uart 78 | 79 | 80 | if __name__ == '__main__': 81 | from pycircuit.formats import * 82 | from pycircuit.build import Builder 83 | 84 | Builder(led()).compile() 85 | Builder(rgb()).compile() 86 | Builder(mcu()).compile() 87 | -------------------------------------------------------------------------------- /pycircuit/library/old/ti/tps6229x.py: -------------------------------------------------------------------------------- 1 | from pycircuit.device import * 2 | from pycircuit.footprint import * 3 | from pycircuit.circuit import * 4 | 5 | 6 | Device('TPS6229x', descr='''1A Step-Down Converter''', pins=[ 7 | PwrIn('VIN', descr='''VIN power supply pin.'''), 8 | 9 | In('EN', descr='''This is the enable pin of the device. Pulling this pin to 10 | low forces the device into shutdown mode. Pulling this pin to high enables 11 | the device. This pin must be terminated.'''), 12 | 13 | Pwr('GND', descr='''GND supply pin.'''), 14 | 15 | In('MODE', descr='''High forces the device to operate in fixed-frequency PWM 16 | mode. When Low enables the power save mode with automatic transition from 17 | PFM mode to fixed-frequency PWM mode.'''), 18 | 19 | PwrOut('SW', descr='''This is the switch pin and is connected to the 20 | internal MOSFET switches. Connect the external inductor between this 21 | terminal and the output capacitor.'''), 22 | 23 | In('FB', descr='''Feedback pin for the internal regulation loop. Connect 24 | the external resistor divider to this pin. In case of fixed output voltage 25 | option, connect this pin directly to the output capacitor.'''), 26 | 27 | Pwr('EP', descr='''Connect the exposed thermal pad to GND.''') 28 | ]) 29 | 30 | #Device('TPS62290', adjustable=True) 31 | #Device('TPS62291', vout=3.3) 32 | #Device('TPS62293', vout=1.8) 33 | 34 | Footprint('TPS6229x', 'TPS6229x', 'QFN16', # 'SON', 35 | Map(1, 'SW'), 36 | Map(2, 'MODE'), 37 | Map(3, 'FB'), 38 | Map(4, 'EN'), 39 | Map(5, 'VIN'), 40 | Map(6, 'GND'), 41 | Map(7, 'EP')) 42 | 43 | 44 | @circuit('TPS6229x') 45 | def tps6229x(dev='TPS6229x', en=True, mode=True, fb=False): 46 | 47 | Node('U', dev) 48 | 49 | Net('GND') + Ref('U')['GND', 'EP'] 50 | Net('VIN') + Ref('U')['VIN'] 51 | 52 | Node('CIN', 'C', '10uF') 53 | Nets('VIN', 'GND') + Ref('CIN')['~', '~'] 54 | 55 | Node('L', 'L', '2.2mH') 56 | Net('SW') + Ref('U')['SW'] + Ref('L')['~'] 57 | Net('VOUT') + Ref('L')['~'] 58 | 59 | Node('COUT', 'C', '10uF') 60 | Nets('VOUT', 'GND') + Ref('COUT')['~', '~'] 61 | 62 | if en: 63 | Net('VIN') + Ref('U')['EN'] 64 | 65 | if mode: 66 | Net('GND') + Ref('U')['MODE'] 67 | 68 | if fb: 69 | Node('FB_R1', 'R', fb[0]) 70 | Node('FB_R2', 'R', fb[1]) 71 | Node('FB_C', 'C', '22pF') 72 | Net('VOUT') + Ref('FB_R1')['~'] + Ref('FB_C')['~'] 73 | Net('FB') + Ref('U')['FB'] + Ref('FB_R1')['~'] + \ 74 | Ref('FB_C')['~'] + Ref('FB_R2')['~'] 75 | else: 76 | Net('VOUT') + Ref('U')['FB'] 77 | -------------------------------------------------------------------------------- /placer/place.py: -------------------------------------------------------------------------------- 1 | from pycircuit.pcb import Pcb 2 | from pycircuit.package import Courtyard 3 | from pycircuit.formats import json 4 | 5 | from z3 import * 6 | from placer.box import Z3Box 7 | from placer.bin import Z3Bin 8 | from placer.grid import Grid 9 | 10 | 11 | class Placer(object): 12 | def __init__(self, grid_size=Courtyard.IPC_GRID_SCALE, density=1.5): 13 | assert density > 1 14 | self.grid_size = grid_size 15 | self.density = density 16 | 17 | def place(self, filein, fileout): 18 | self.pcb = Pcb.from_file(filein) 19 | left, bottom, right, top = self.pcb.outline.polygon.interiors[0].bounds 20 | width, height = (right - left) / \ 21 | self.grid_size, (top - bottom) / self.grid_size 22 | print('width', width, 'height', height) 23 | 24 | boxes = [] 25 | min_area = 0 26 | 27 | for inst in self.pcb.netlist.insts: 28 | box = Z3Box.from_inst(inst) 29 | min_area += box.area() 30 | boxes.append(box) 31 | 32 | pcb = Z3Bin() 33 | 34 | s = Solver() 35 | 36 | s.add(pcb.range_constraint()) 37 | 38 | for i, box in enumerate(boxes): 39 | # Add rotation constraints 40 | s.add(box.rotation_constraint()) 41 | # Constrain position to be on the pcb 42 | s.add(box.range_constraint(pcb)) 43 | for j in range(i): 44 | # Add non overlapping constraint 45 | s.add(box.overlap_constraint(boxes[j])) 46 | 47 | # Project constraints: 48 | s.add(pcb.var_width <= width) 49 | s.add(pcb.var_height <= height) 50 | s.add(pcb.area_constraint(min_area * self.density)) 51 | 52 | # s.add(boxes[0].fix_position_constraint(*pcb.var_center())) 53 | 54 | if s.check() == sat: 55 | model = s.model() 56 | 57 | pcb.eval(model) 58 | print(str(pcb)) 59 | 60 | grid = Grid(*pcb.dim()) 61 | 62 | for box in boxes: 63 | box.eval(model) 64 | print(str(box)) 65 | grid.add_box(box) 66 | box.place_inst((left, top), self.grid_size) 67 | 68 | print(str(grid)) 69 | 70 | self.pcb.to_file(fileout) 71 | else: 72 | print('unsat') 73 | 74 | 75 | if __name__ == '__main__': 76 | import argparse 77 | 78 | parser = argparse.ArgumentParser( 79 | description='SMT-based, constrained placement') 80 | 81 | parser.add_argument('filein', type=str) 82 | parser.add_argument('fileout', type=str) 83 | 84 | args, unknown = parser.parse_known_args() 85 | 86 | placer = Placer() 87 | placer.place(args.filein, args.fileout) 88 | -------------------------------------------------------------------------------- /tests/test_component.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pycircuit.component import * 3 | 4 | 5 | class FunTests(unittest.TestCase): 6 | 7 | def test_name_only(self): 8 | f = Fun('GPIO') 9 | assert f.function == 'GPIO' 10 | 11 | def test_name_and_dir(self): 12 | f = Fun('VCC') 13 | assert f.function == 'VCC' 14 | 15 | def test_bus(self): 16 | f = BusFun('UART0', 'UART_TX') 17 | assert f.bus == 'UART0' 18 | assert f.function == 'UART_TX' 19 | 20 | 21 | class PinTests(unittest.TestCase): 22 | 23 | def test_simple_pin(self): 24 | pin = Pin('GPIO') 25 | assert pin.name == 'GPIO' 26 | assert pin.description == '' 27 | assert len(pin.funs) == 1 28 | 29 | def test_pin_with_descr(self): 30 | pin = Pin('GPIO', description='A GPIO Pin') 31 | assert pin.description == 'A GPIO Pin' 32 | 33 | def test_pin_with_fun(self): 34 | pin = Pin('GPIO_1', Fun('GPIO'), BusFun( 35 | 'UART0', 'UART_TX'), description='A GPIO Pin') 36 | assert pin.name == 'GPIO_1' 37 | assert pin.description == 'A GPIO Pin' 38 | assert len(pin.funs) == 2 39 | assert pin.funs[0].function == 'GPIO' 40 | assert pin.funs[1].function == 'UART_TX' 41 | 42 | def test_function_by_name(self): 43 | pin = Pin('GPIO_1', Fun('GPIO')) 44 | assert pin.has_function('GPIO') == True 45 | 46 | 47 | class ComponentTests(unittest.TestCase): 48 | 49 | def test_component(self): 50 | cmp = Component('MCU', 'Microcontroller', 51 | Pin('GND'), 52 | Pin('VCC'), 53 | Pin('XTAL_XI'), 54 | Pin('XTAL_XO'), 55 | Pin('JTAG_TCK'), 56 | Pin('JTAG_TDO'), 57 | Pin('JTAG_TMS'), 58 | Pin('JTAG_TDI'), 59 | Pin('GPIO_1', Fun('GPIO'), BusFun('UART0', 'UART_TX')), 60 | Pin('GPIO_2', Fun('GPIO'), BusFun('UART0', 'UART_RX')), 61 | Pin('GPIO_3', Fun('GPIO')), 62 | Pin('GPIO_4', Fun('GPIO')), 63 | Pin('GPIO_5', Fun('GPIO'), BusFun('UART1', 'UART_TX')), 64 | Pin('GPIO_6', Fun('GPIO'), BusFun('UART1', 'UART_RX'))) 65 | 66 | assert cmp.name == 'MCU' 67 | assert cmp.description == 'Microcontroller' 68 | assert len(cmp.pins) == 14 69 | assert len(cmp.busses) == 2 70 | 71 | gnd = cmp.pin_by_name('GND') 72 | assert len(gnd.funs) == 1 73 | 74 | gpio_1 = cmp.pin_by_name('GPIO_1') 75 | assert gpio_1.name == 'GPIO_1' 76 | assert len(gpio_1.funs) == 2 77 | -------------------------------------------------------------------------------- /tests/test_device.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | from pycircuit.device import Device, Map 4 | from pycircuit.library import * 5 | 6 | 7 | class DeviceTests(unittest.TestCase): 8 | def setUp(self): 9 | Device.devices = [] 10 | 11 | def test_works(self): 12 | Device('R0805', 'R', '0805', 13 | Map('1', 'A'), 14 | Map('2', 'B')) 15 | 16 | def test_none_none_map(self): 17 | with pytest.raises(Exception): 18 | Map(None, None) 19 | 20 | def test_component_doesnt_exist(self): 21 | with pytest.raises(Exception): 22 | Device('R0805', 'R1', '0805', 23 | Map('1', 'A'), 24 | Map('2', 'B')) 25 | 26 | def test_package_doesnt_exist(self): 27 | with pytest.raises(Exception): 28 | Device('R0805', 'R', 'R0805', 29 | Map('1', 'A'), 30 | Map('2', 'B')) 31 | 32 | def test_pin_doesnt_exist(self): 33 | with pytest.raises(Exception): 34 | Device('R0805', 'R', '0805', 35 | Map('1', 'A'), 36 | Map('2', 'C')) 37 | 38 | def test_pad_doesnt_exist(self): 39 | with pytest.raises(Exception): 40 | Device('R0805', 'R', '0805', 41 | Map('1', 'A'), 42 | Map('3', 'B')) 43 | 44 | def test_works2(self): 45 | Device('SOT23BCE', 'Q', 'SOT23', 46 | Map('1', 'B'), 47 | Map('2', 'C'), 48 | Map('3', 'E'), 49 | Map(None, 'SUBSTRATE')) 50 | 51 | def test_pin_unmapped(self): 52 | with pytest.raises(Exception): 53 | Device('SOT23BCE', 'Q', 'SOT23', 54 | Map('1', 'B'), 55 | Map('2', 'C'), 56 | Map('3', 'E')) 57 | 58 | def test_pin_required(self): 59 | with pytest.raises(Exception): 60 | Device('SOT23BCE', 'Q', 'SOT23', 61 | Map('1', 'B'), 62 | Map('2', 'C'), 63 | Map('3', 'SUBSTRATE'), 64 | Map(None, 'E')) 65 | 66 | def pad_mapped_multiple(self): 67 | with pytest.raises(Exception): 68 | Device('SOT23BCE', 'Q', 'SOT23', 69 | Map('1', 'B'), 70 | Map('2', 'C'), 71 | Map('3', 'E'), 72 | Map('3', 'SUBSTRATE')) 73 | 74 | def test_works3(self): 75 | Device('RSOT23', 'R', 'SOT23', 76 | Map('1', 'A'), 77 | Map('2', 'B'), 78 | Map('3', None)) 79 | 80 | def test_pad_unmapped(self): 81 | with pytest.raises(Exception): 82 | Device('RSOT23', 'R', 'SOT23', 83 | Map('1', 'A'), 84 | Map('2', 'B')) 85 | -------------------------------------------------------------------------------- /tests/test_pinassign.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pycircuit.component import * 3 | from pycircuit.pinassign import * 4 | 5 | Component('R', 'Resistor', 6 | Pin('1', Fun('~')), 7 | Pin('2', Fun('~')) 8 | ) 9 | 10 | Component('MCU', 'Microcontroller', 11 | Pin('VCC'), 12 | Pin('GPIO0', Fun('GPIO'), BusFun('UART0', 'UART_TX')), 13 | Pin('GPIO1', Fun('GPIO'), BusFun('UART0', 'UART_RX')), 14 | Pin('GPIO2', Fun('GPIO'), BusFun('UART1', 'UART_TX')), 15 | Pin('GPIO3', Fun('GPIO'), BusFun('UART1', 'UART_RX')) 16 | ) 17 | 18 | Component('3x0806', '2 Resistors and one Capacitor', 19 | Pin('P1.1', BusFun('P1', '~'), BusFun('P1', '+'), BusFun('P1', '-')), 20 | Pin('P1.2', BusFun('P1', '~'), BusFun('P1', '+'), BusFun('P1', '-')), 21 | Pin('P2.1', BusFun('P2', '~'), BusFun('P2', '+'), BusFun('P2', '-')), 22 | Pin('P2.2', BusFun('P2', '~'), BusFun('P2', '+'), BusFun('P2', '-')), 23 | Pin('P3.1', BusFun('P3', '~'), BusFun('P3', '+'), BusFun('P3', '-')), 24 | Pin('P3.2', BusFun('P3', '~'), BusFun('P3', '+'), BusFun('P3', '-')), 25 | ) 26 | 27 | 28 | class PinAssignTests(unittest.TestCase): 29 | def test_obvious_assign(self): 30 | component = Component.component_by_name('R') 31 | problem = AssignmentProblem(component, ( 32 | Z3Assign('~', 0), 33 | Z3Assign('~', 1), 34 | )) 35 | 36 | problem.solve() 37 | problem.print_solution() 38 | assert problem.check_solution() is None 39 | 40 | def test_simple_assign(self): 41 | component = Component.component_by_name('MCU') 42 | problem = AssignmentProblem(component, ( 43 | Z3Assign('GPIO', 0), 44 | Z3Assign('GPIO', 1), 45 | Z3Assign('UART_TX', 2), 46 | Z3Assign('UART_RX', 3), 47 | Z3Assign('VCC', 4) 48 | )) 49 | 50 | problem.solve() 51 | problem.print_solution() 52 | assert problem.check_solution() is None 53 | 54 | def test_bus_assign(self): 55 | component = Component.component_by_name('MCU') 56 | problem = AssignmentProblem(component, ( 57 | Z3Assign('GPIO', 0), 58 | Z3Assign('GPIO', 1), 59 | Z3BusAssign( 60 | Z3Assign('UART_TX', 2), 61 | Z3Assign('UART_RX', 3) 62 | ), 63 | Z3Assign('VCC', 4) 64 | )) 65 | 66 | problem.solve() 67 | problem.print_solution() 68 | assert problem.check_solution() is None 69 | 70 | def test_multiple_bus_assign(self): 71 | component = Component.component_by_name('3x0806') 72 | problem = AssignmentProblem(component, ( 73 | Z3BusAssign( 74 | Z3Assign('~', 0), 75 | Z3Assign('~', 1) 76 | ), 77 | Z3BusAssign( 78 | Z3Assign('~', 2), 79 | Z3Assign('~', 3) 80 | ), 81 | Z3BusAssign( 82 | Z3Assign('+', 4), 83 | Z3Assign('-', 5) 84 | ) 85 | )) 86 | 87 | problem.solve() 88 | problem.print_solution() 89 | assert problem.check_solution() is None 90 | -------------------------------------------------------------------------------- /pycircuit/library/components.py: -------------------------------------------------------------------------------- 1 | from pycircuit.component import * 2 | 3 | 4 | # Passive Devices 5 | Component('Z', 'Impedance', 6 | Pin('A', Fun('~'), optional=False), 7 | Pin('B', Fun('~'), optional=False)) 8 | 9 | Component('R', 'Resistor', 10 | Pin('A', Fun('~'), optional=False), 11 | Pin('B', Fun('~'), optional=False)) 12 | 13 | Component('C', 'Capacitor', 14 | Pin('A', BusFun('Ceramic', '~'), 15 | BusFun('Electrolytic', '+'), optional=False), 16 | Pin('B', BusFun('Ceramic', '~'), 17 | BusFun('Electrolytic', '-'), optional=False)) 18 | 19 | Component('L', 'Inductor', 20 | Pin('A', Fun('~'), optional=False), 21 | Pin('B', Fun('~'), optional=False)) 22 | 23 | Component('V', 'Voltage source', 24 | Pin('+', optional=False), 25 | Pin('-', optional=False)) 26 | 27 | Component('S', 'Switch', 28 | Pin('A', Fun('~'), optional=False), 29 | Pin('B', Fun('~'), optional=False)) 30 | 31 | Component('XTAL', 'Crystal', 32 | Pin('A', Fun('~'), optional=False), 33 | Pin('B', Fun('~'), optional=False)) 34 | 35 | Component('TP', 'Test point', 36 | In('TP', optional=False)) 37 | 38 | Component('J2P', 'Jumper 2-pins', 39 | Pin('A', Fun('~'), optional=False), 40 | Pin('B', Fun('~'), optional=False)) 41 | 42 | Component('J3P', 'Jumper 3-pins', 43 | Pin('A', Fun('~'), optional=False), 44 | Pin('B', Fun('~'), optional=False), 45 | Pin('C', optional=False)) 46 | 47 | Component('ANT', 'Antenna', Pin('ANT', optional=False)) 48 | 49 | Component('Transformer_1P_1S', 'Transformer with one primary and one secondary winding', 50 | Pin('L1.1', optional=False), 51 | Pin('L1.2', optional=False), 52 | Pin('L2.1', optional=False), 53 | Pin('L2.2', optional=False)) 54 | 55 | 56 | # Active Devices 57 | Component('D', 'Diode', 58 | Pin('+', optional=False), 59 | Pin('-', optional=False)) 60 | 61 | Component('Q', 'Bipolar transistor', 62 | Pin('B', optional=False), 63 | Pin('C', optional=False), 64 | Pin('E', optional=False), 65 | Pin('SUBSTRATE')) 66 | 67 | Component('M', 'Mosfet', 68 | Pin('G', optional=False), 69 | Pin('D', optional=False), 70 | Pin('S', optional=False), 71 | Pin('SUBSTRATE')) 72 | 73 | Component('OP', 'Opamp', 74 | Pwr('VCC', optional=False), 75 | Pwr('VEE', optional=False), 76 | In('+', optional=False), 77 | In('-', optional=False), 78 | Out('OUT', optional=False)) 79 | 80 | Component('RGB_A', 'RGB LED (Common Anode)', 81 | In('+', optional=False), 82 | Out('R', optional=False), 83 | Out('G', optional=False), 84 | Out('B', optional=False)) 85 | 86 | Component('RGB_C', 'RGB LED (Common Cathode)', 87 | In('R', optional=False), 88 | In('G', optional=False), 89 | In('B', optional=False), 90 | Out('-', optional=False)) 91 | 92 | Component('CLK', 'Clock', 93 | Pwr('VDD', optional=False), 94 | Gnd('GND', optional=False), 95 | Out('CLK', optional=False)) 96 | 97 | Component('QSPI:S', 'Quad SPI Slave', 98 | Pwr('VDD', optional=False), 99 | Gnd('GND', optional=False), 100 | In('SCLK', optional=False), 101 | Io('DQ0'), 102 | Io('DQ1'), 103 | Io('DQ2'), 104 | Io('DQ3'), 105 | In('SS', optional=False)) 106 | 107 | Component('I2C:S', 'I2C Slave', 108 | Pwr('VDD', optional=False), 109 | Gnd('GND', optional=False), 110 | Io('SDA', optional=False), 111 | In('SCL', optional=False)) 112 | -------------------------------------------------------------------------------- /pycircuit/optimize.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.signal 3 | import ngspyce 4 | from pycircuit.testbench import testbench 5 | from pycircuit.build import Builder 6 | from pycircuit.diffev import DiffEvolver 7 | 8 | 9 | class Optimizer(object): 10 | def __init__(self, netlist, spec, nsamples=20, npop=50): 11 | self.netlist = netlist 12 | self.spec = spec 13 | 14 | # Find simulation parameters from specification 15 | self.nsamples = nsamples 16 | self.sim_params = self.find_sim_params() 17 | 18 | # Find all resistors and capacitors in netlist 19 | # to determine problem size 20 | self.resistors = [] 21 | self.capacitors = [] 22 | 23 | for inst in netlist.iter_insts(): 24 | if inst.component.name == 'R': 25 | self.resistors.append(inst) 26 | elif inst.component.name == 'C': 27 | self.capacitors.append(inst) 28 | 29 | self.nparams = len(self.resistors) + len(self.capacitors) 30 | 31 | # Run simulation to find freqs 32 | self.freqs, _ = self.simulate() 33 | # Compute target values from specification 34 | _, self.target = scipy.signal.freqs( 35 | *self.spec, worN=2 * np.pi * self.freqs) 36 | 37 | # Initialize DiffEvolver 38 | lbound = scipy.zeros(self.nparams) 39 | ubound = scipy.ones(self.nparams) 40 | self.de = DiffEvolver.frombounds(self.cost, lbound, ubound, npop) 41 | self.de.set_boundaries(lbound, ubound, mode='mirror') 42 | 43 | def find_sim_params(self): 44 | w = scipy.signal.findfreqs(*self.spec, self.nsamples) 45 | fstart = w[0] / 2 / np.pi 46 | fstop = w[-1] / 2 / np.pi 47 | decades = np.log10(fstop) - np.log10(fstart) 48 | npoints = self.nsamples / decades 49 | return { 50 | 'mode': 'dec', 51 | 'npoints': npoints, 52 | 'fstart': fstart, 53 | 'fstop': fstop 54 | } 55 | 56 | def transform(self, vector): 57 | for ir, r in enumerate(self.resistors): 58 | value = 10 ** (vector[ir] * 3 + 3) # 1K to 1M 59 | r.set_value(value) 60 | for ic, c in enumerate(self.capacitors): 61 | value = 10 ** (vector[ir + ic] * 6 - 9) # 1nF to 1mF 62 | c.set_value(value) 63 | 64 | def simulate(self): 65 | self.netlist.to_spice('optimize.sp') 66 | ngspyce.source('optimize.sp') 67 | ngspyce.ac(**self.sim_params) 68 | fs = np.abs(ngspyce.vector('frequency')) 69 | vs = ngspyce.vector('vout') 70 | return fs, vs 71 | 72 | def _vector_to_values(self, vector): 73 | self.transform(vector) 74 | _, values = self.simulate() 75 | return values 76 | 77 | def cost(self, vector): 78 | values = self._vector_to_values(vector) 79 | return sum(abs(self.target - values) ** 2) 80 | 81 | def plot_result(self): 82 | values = self._vector_to_values(self.de.best_vector) 83 | self.plot_freq_resp(values) 84 | 85 | def plot_freq_resp(self, values): 86 | from matplotlib import pyplot as plt 87 | 88 | fig, (ax1, ax2) = plt.subplots(2) 89 | fig.suptitle('Frequency response') 90 | 91 | ax1.semilogx(self.freqs, 20 * np.log10(abs(self.target))) 92 | ax1.semilogx(self.freqs, 20 * np.log10(abs(values))) 93 | ax1.set_xlabel('Frequency [Hz]') 94 | ax1.set_ylabel('Amplitude [dB]') 95 | ax1.grid(which='both', axis='both') 96 | 97 | ax2.semilogx(self.freqs, np.angle(self.target, True)) 98 | ax2.semilogx(self.freqs, np.angle(values, True)) 99 | ax2.set_xlabel('Frequency [Hz]') 100 | ax2.set_ylabel('Phase [degrees]') 101 | ax2.grid(which='both', axis='both') 102 | 103 | plt.show() 104 | 105 | def optimize(self): 106 | self.de.solve(10) 107 | min_cost = self.cost(self.de.best_vector) 108 | return min_cost 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dvc94ch/pycircuit.svg?branch=master)](https://travis-ci.org/dvc94ch/pycircuit) 2 | # Circuit Description Library 3 | 4 | ## Getting started 5 | 6 | `common_emitter.py` 7 | ```python 8 | from pycircuit.circuit import * 9 | from pycircuit.library import * 10 | 11 | 12 | @circuit('Common Emitter', 'gnd', '12V', 'vin', 'vout') 13 | def common_emitter_amplifer(self, gnd, vcc, vin, vout): 14 | nb, ne = nets('nb ne') 15 | Inst('Q', 'npn sot23')['B', 'C', 'E'] = nb, vout, ne 16 | 17 | # Current limiting resistor 18 | Inst('R', '1.2k')['~', '~'] = vcc, vout 19 | 20 | # Thermal stabilization (leads to a gain reduction) 21 | Inst('R', '220')['~', '~'] = ne, gnd 22 | # Shorts Re for AC signal (increases gain) 23 | Inst('C', '10uF')['~', '~'] = ne, gnd 24 | 25 | # Biasing resistors 26 | Inst('R', '20k')['~', '~'] = vcc, nb 27 | Inst('R', '3.6k')['~', '~'] = nb, gnd 28 | # Decoupling capacitor 29 | Inst('C', '10uF')['~', '~'] = vin, nb 30 | 31 | 32 | if __name__ == '__main__': 33 | from pycircuit.formats import * 34 | from pycircuit.build import Builder 35 | 36 | Builder(common_emitter_amplifer()).compile() 37 | ``` 38 | 39 | 40 | ![Schematic](https://user-images.githubusercontent.com/741807/34790831-53fb6d02-f643-11e7-895e-2c12e81b69c7.png) 41 | 42 | ## Optimization 43 | `sallen_key/build.py` 44 | ```python 45 | import numpy as np 46 | import scipy.signal as sig 47 | from pycircuit.build import Builder 48 | from pycircuit.circuit import testbench 49 | from pycircuit.optimize import Optimizer 50 | 51 | from sallen_key import lp_sallen_key 52 | 53 | def lp_optimize(): 54 | spec = sig.butter(2, 2 * np.pi * 100, btype='low', analog=True) 55 | tb = Builder(testbench(lp_sallen_key())).compile() 56 | problem = Optimizer(tb, spec) 57 | cost = problem.optimize() 58 | print(cost) 59 | problem.plot_result() 60 | print(repr(problem.netlist)) 61 | 62 | 63 | if __name__ == '__main__': 64 | lp_optimize() 65 | ``` 66 | 67 | After 10s runtime. If it's not good enough run it again... 68 | ![Optimizer](https://user-images.githubusercontent.com/741807/34791214-943eb1ca-f644-11e7-991a-41c7727d9e62.png) 69 | 70 | ## Physical design 71 | `joule_thief/build.py` 72 | ```python 73 | import joule_thief 74 | from pycircuit.build import Builder 75 | from pycircuit.library.design_rules import oshpark_4layer 76 | from placer import Placer 77 | from router import Router 78 | from pykicad.pcb import Zone 79 | 80 | def place(filein, fileout): 81 | placer = Placer() 82 | placer.place(filein, fileout) 83 | 84 | 85 | def route(filein, fileout): 86 | router = Router() 87 | router.route(filein, fileout) 88 | 89 | 90 | def post_process(pcb, kpcb): 91 | xmin, ymin, xmax, ymax = pcb.boundary() 92 | coords = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] 93 | 94 | zone = Zone(net_name='GND', layer='F.Cu', 95 | polygon=coords, clearance=0.3) 96 | 97 | kpcb.zones.append(zone) 98 | return kpcb 99 | 100 | 101 | if __name__ == '__main__': 102 | Builder(joule_thief.top(), oshpark_4layer, 103 | place=place, route=route, post_process=post_process).build() 104 | ``` 105 | 106 | ![KiCad](https://user-images.githubusercontent.com/741807/34364057-43e7ee62-ea82-11e7-9787-84fefaecbc49.png) 107 | 108 | 109 | # License 110 | ISC License 111 | 112 | Copyright (c) 2017, David Craven 113 | 114 | Permission to use, copy, modify, and/or distribute this software for any 115 | purpose with or without fee is hereby granted, provided that the above 116 | copyright notice and this permission notice appear in all copies. 117 | 118 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 119 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 120 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 121 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 122 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 123 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 124 | PERFORMANCE OF THIS SOFTWARE. 125 | -------------------------------------------------------------------------------- /pycircuit/library/old/ftdi/ftdi.py: -------------------------------------------------------------------------------- 1 | from pycircuit.component import * 2 | from pycircuit.footprint import * 3 | 4 | 5 | # FT232 6 | Component('FT2232H', 'FTDI chip', ( 7 | # Power and ground 8 | Pin('VCORE'), # 1V8 9 | Pin('VCCIO'), # 3V3 10 | Pin('VPLL'), # 3V3 11 | Pin('VPHY'), # 3V3 12 | Pin('VREGIN'), 13 | Pin('VREGOUT'), 14 | Pin('AGND'), 15 | Pin('GND'), 16 | # Common pins 17 | Pin('OSCI'), # 12MHz 18 | Pin('OSCO'), # 12MHz 19 | Pin('REF'), 20 | Pin('DN', BusFun('USB0', 'USB', 'D-')), 21 | Pin('DP', BusFun('USB0', 'USB', 'D+')), 22 | Pin('TEST'), 23 | Pin('RESET'), 24 | Pin('PWREN'), 25 | Pin('SUSPEND'), 26 | # EEPROM 27 | Pin('EECS'), 28 | Pin('EECLK'), 29 | Pin('EEDATA'), 30 | # Channel A 31 | Pin('ADBUS0', 32 | BusFun('UART0', 'UART', 'TX'), 33 | BusFun('JTAG0', 'JTAG', 'TCK'), 34 | BusFun('SPI0', 'SPI', 'SCK')), 35 | Pin('ADBUS1', 36 | BusFun('UART0', 'UART', 'RX'), 37 | BusFun('JTAG0', 'JTAG', 'TDI'), 38 | BusFun('SPI0', 'SPI', 'MISO')), 39 | Pin('ADBUS2', 40 | BusFun('JTAG0', 'JTAG', 'TDO'), 41 | BusFun('SPI0', 'SPI', 'MOSI')), 42 | Pin('ADBUS3', 43 | BusFun('JTAG0', 'JTAG', 'TMS'), 44 | BusFun('SPI0', 'SPI', 'SS0')), 45 | Pin('ADBUS4'), 46 | Pin('ADBUS5'), 47 | Pin('ADBUS6'), 48 | Pin('ADBUS7'), 49 | Pin('ACBUS0'), 50 | Pin('ACBUS1'), 51 | Pin('ACBUS2'), 52 | Pin('ACBUS3'), 53 | Pin('ACBUS4'), 54 | Pin('ACBUS5'), 55 | Pin('ACBUS6'), 56 | Pin('ACBUS7'), 57 | # Channel B 58 | Pin('BDBUS0', 59 | BusFun('UART1', 'UART', 'TX'), 60 | BusFun('JTAG1', 'JTAG', 'TCK'), 61 | BusFun('SPI1', 'SPI', 'SCK')), 62 | Pin('BDBUS1', 63 | BusFun('UART1', 'UART', 'RX'), 64 | BusFun('JTAG1', 'JTAG', 'TDI'), 65 | BusFun('SPI1', 'SPI', 'MISO')), 66 | Pin('BDBUS2', 67 | BusFun('JTAG1', 'JTAG', 'TDO'), 68 | BusFun('SPI1', 'SPI', 'MOSI')), 69 | Pin('BDBUS3', 70 | BusFun('JTAG1', 'JTAG', 'TMS'), 71 | BusFun('SPI1', 'SPI', 'SS0')), 72 | Pin('BDBUS4'), 73 | Pin('BDBUS5'), 74 | Pin('BDBUS6'), 75 | Pin('BDBUS7'), 76 | Pin('BCBUS0'), 77 | Pin('BCBUS1'), 78 | Pin('BCBUS2'), 79 | Pin('BCBUS3'), 80 | Pin('BCBUS4'), 81 | Pin('BCBUS5'), 82 | Pin('BCBUS6'), 83 | Pin('BCBUS7', Fun('PWRSAV')) 84 | )) 85 | 86 | 87 | Footprint('FT2232HQ', 'FT2232H', 'QFN64', 88 | Map(1, 'GND'), 89 | Map(2, 'OSCI'), 90 | Map(3, 'OSCO'), 91 | Map(4, 'VPHY'), 92 | Map(5, 'GND'), 93 | Map(6, 'REF'), 94 | Map(7, 'DM'), 95 | Map(8, 'DP'), 96 | Map(9, 'VPLL'), 97 | Map(10, 'AGND'), 98 | Map(11, 'GND'), 99 | Map(12, 'VCORE'), 100 | Map(13, 'TEST'), 101 | Map(14, 'RESET'), 102 | Map(15, 'GND'), 103 | Map(16, 'ADBUS0'), 104 | Map(17, 'ADBUS1'), 105 | Map(18, 'ADBUS2'), 106 | Map(19, 'ADBUS3'), 107 | Map(20, 'VCCIO'), 108 | Map(21, 'ADBUS4'), 109 | Map(22, 'ADBUS5'), 110 | Map(23, 'ADBUS6'), 111 | Map(24, 'ADBUS7'), 112 | Map(25, 'GND'), 113 | Map(26, 'ACBUS0'), 114 | Map(27, 'ACBUS1'), 115 | Map(28, 'ACBUS2'), 116 | Map(29, 'ACBUS3'), 117 | Map(30, 'ACBUS4'), 118 | Map(31, 'VCCIO'), 119 | Map(32, 'ACBUS5'), 120 | Map(33, 'ACBUS6'), 121 | Map(34, 'ACBUS7'), 122 | Map(35, 'GND'), 123 | Map(36, 'SUSPEND'), 124 | Map(37, 'VCORE'), 125 | Map(38, 'BDBUS0'), 126 | Map(39, 'BDBUS1'), 127 | Map(40, 'BDBUS2'), 128 | Map(41, 'BDBUS3'), 129 | Map(42, 'VCCIO'), 130 | Map(43, 'BDBUS4'), 131 | Map(44, 'BDBUS5'), 132 | Map(45, 'BDBUS6'), 133 | Map(46, 'BDBUS7'), 134 | Map(47, 'GND'), 135 | Map(48, 'BCBUS0'), 136 | Map(49, 'VREGOUT'), 137 | Map(50, 'VREGIN'), 138 | Map(51, 'GND'), 139 | Map(52, 'BCBUS1'), 140 | Map(53, 'BCBUS2'), 141 | Map(54, 'BCBUS3'), 142 | Map(55, 'BCBUS4'), 143 | Map(56, 'VCCIO'), 144 | Map(57, 'BCBUS5'), 145 | Map(58, 'BCBUS6'), 146 | Map(59, 'BCBUS7'), 147 | Map(60, 'PWREN'), 148 | Map(61, 'EEDATA'), 149 | Map(62, 'EECLK'), 150 | Map(63, 'EECS'), 151 | Map(64, 'VCORE')) 152 | -------------------------------------------------------------------------------- /pycircuit/device.py: -------------------------------------------------------------------------------- 1 | from pycircuit.component import * 2 | from pycircuit.package import * 3 | 4 | 5 | class Map(object): 6 | '''Represents the a map from a Pad to a Pin.''' 7 | 8 | def __init__(self, pad, pin): 9 | '''A Map has a pad name, a pin name and a Footprint.''' 10 | assert not (pad is None and pin is None) 11 | self.pad = pad 12 | self.pin = pin 13 | self.device = None 14 | 15 | def __str__(self): 16 | '''Returns the name of the pad.''' 17 | 18 | return self.pad 19 | 20 | def __repr__(self): 21 | '''Returns a string "pad <> pin".''' 22 | 23 | return '%4s <> %s' % (self.pad, self.pin) 24 | 25 | 26 | class Device(object): 27 | '''Represents a mapping from a Component to a Package.''' 28 | 29 | devices = [] 30 | 31 | def __init__(self, name, component, package, *maps): 32 | '''A Device with name that maps the pads of a Package to the pins 33 | of a Component.''' 34 | 35 | self.name = name 36 | self.component = Component.component_by_name(component) 37 | self.package = Package.package_by_name(package) 38 | self.maps = [] 39 | 40 | for map in maps: 41 | self.add_map(map) 42 | 43 | self.check_device() 44 | self.register_device(self) 45 | 46 | def add_map(self, map): 47 | '''Adds a map to Device.''' 48 | 49 | pin = map.pin 50 | pad = map.pad 51 | 52 | if pin is not None: 53 | pin = self.component.pin_by_name(pin) 54 | if pin is None: 55 | print('Warn: Device %s: Pin %s not in component %s' 56 | % (self.name, map.pin, self.component.name)) 57 | if pad is not None: 58 | pad = self.package.pad_by_name(pad) 59 | if pad is None: 60 | print('Warn: Device %s: Pad %s not in package %s' 61 | % (self.name, map.pad, self.package.name)) 62 | 63 | if pad is None: 64 | assert pin.optional 65 | 66 | map.pin = pin 67 | map.pad = pad 68 | map.device = self 69 | self.maps.append(map) 70 | 71 | def check_device(self): 72 | '''Checks that every Pin and every Pad has a Map.''' 73 | 74 | for pin in self.component.pins: 75 | for map in self.maps: 76 | if map.pin == pin: 77 | break 78 | else: 79 | raise AssertionError('No map for component %s pin %s in device %s' 80 | % (self.component.name, pin.name, self.name)) 81 | for pad in self.package.pads: 82 | for map in self.maps: 83 | if map.pad == pad: 84 | break 85 | else: 86 | raise AssertionError('No map for package %s pad %s in device %s' 87 | % (self.package.name, pad.name, self.name)) 88 | 89 | def pin_by_pad(self, pad): 90 | '''Returns the pin mapped to pad.''' 91 | 92 | for map in self.maps: 93 | if map.pad == pad: 94 | return map.pin 95 | 96 | def pads_by_pin(self, pin): 97 | '''Returns a list of pads mapped to pin.''' 98 | 99 | for map in self.maps: 100 | if map.pin == pin: 101 | yield map.pad 102 | 103 | def __str__(self): 104 | '''Returns the name of the Device.''' 105 | 106 | return self.name 107 | 108 | def __repr__(self): 109 | '''Returns a string representation of a Device.''' 110 | 111 | return '%s (%s <> %s)\n%s\n' % (self.name, self.component, self.package, 112 | '\n'.join([repr(map) for map in self.maps])) 113 | 114 | @classmethod 115 | def device_by_name(cls, name): 116 | '''Returns the Device with name from registered devices.''' 117 | 118 | for dev in cls.devices: 119 | if dev.name == name: 120 | return dev 121 | 122 | raise IndexError('No Device with name ' + name) 123 | 124 | @classmethod 125 | def devices_by_component(cls, component): 126 | '''Returns the available Devices for Component.''' 127 | 128 | for dev in cls.devices: 129 | if dev.component == component: 130 | yield dev 131 | 132 | @classmethod 133 | def register_device(cls, device): 134 | '''Register a Device.''' 135 | 136 | try: 137 | cls.device_by_name(device.name) 138 | raise Exception('Device with name %s already exists' % device.name) 139 | except IndexError: 140 | cls.devices.append(device) 141 | -------------------------------------------------------------------------------- /pycircuit/library/design_rules.py: -------------------------------------------------------------------------------- 1 | from pycircuit.layers import Layers, Layer, Materials 2 | from pycircuit.outline import OutlineDesignRules 3 | from pycircuit.traces import TraceDesignRules 4 | from pycircuit.pcb import PcbAttributes 5 | 6 | 7 | def oshpark_4layer(): 8 | '''Design rules for a OSHPark 4-layer board 9 | 10 | layer stackup: 11 | 1.0 mil (0.0254mm) solder resist +/-0.2mil (0.0051mm) 12 | 1.4 mil (0.0356mm) 1 oz copper 13 | 6.7 mil (0.1702mm) FR408 prepreg +/-.67mil (0.017mm) 14 | 0.7 mil (0.0178mm) 0.5 oz copper 15 | 47 mil (1.1938mm) FR408 core +/-4.7mil (0.1194mm) 16 | 0.7 mil (0.0178mm) 0.5 oz copper 17 | 6.7 mil (0.1702mm) FR408 prepreg +/-.67mil (0.017mm) 18 | 1.4 mil (0.0356mm) 1 oz copper 19 | 1.0 mil (0.0254mm) solder resist +/-0.2mil (0.0051mm) 20 | 21 | outline design rules: 22 | min_drill_size = 10mil (0.254mm) 23 | min_slot_width = 100" (1.7272mm) 24 | min_cutout_size = 100" (1.7272mm) 25 | 26 | net design rules: 27 | min_trace_width = 5mil (0.127mm) 28 | min_clearance = 5mil (0.127mm) 29 | min_via_drill = 10mil (0.254mm) 30 | min_annular_ring = 4mil (0.1016mm) 31 | min_edge_clearance = 15mil (0.381mm) 32 | ''' 33 | 34 | layers = Layers([ 35 | Layer('silk_top', None, Materials.SilkScreen), 36 | Layer('finish_top', None, Materials.ENIG), 37 | Layer('mask_top', 0.0254, Materials.PurpleSolderMask), 38 | 39 | Layer('top', 0.0356, Materials.Cu), 40 | Layer('sub_outer1', 0.1702, Materials.FR408), 41 | 42 | Layer('inner1', 0.0178, Materials.Cu), 43 | Layer('sub_inner', 1.1938, Materials.FR408), 44 | Layer('inner2', 0.0178, Materials.Cu), 45 | 46 | Layer('sub_outer2', 0.1702, Materials.FR408), 47 | Layer('bottom', 0.0356, Materials.Cu), 48 | 49 | Layer('mask_bottom', 0.0254, Materials.PurpleSolderMask), 50 | Layer('finish_bottom', None, Materials.ENIG), 51 | Layer('silk_bottom', None, Materials.SilkScreen), 52 | ]) 53 | 54 | outline_design_rules = OutlineDesignRules( 55 | min_drill_size=0.254, 56 | min_slot_width=1.7272, 57 | min_cutout_size=1.7272 58 | ) 59 | 60 | trace_design_rules = TraceDesignRules( 61 | min_width=0.127, 62 | min_clearance=0.127, 63 | min_drill=0.254, 64 | min_annular_ring=0.1016, 65 | min_edge_clearance=0.381, 66 | blind_vias_allowed=False, 67 | burried_vias_allowed=False 68 | ) 69 | 70 | cost_cm2 = 1.5 71 | return PcbAttributes(layers, outline_design_rules, 72 | trace_design_rules, cost_cm2) 73 | 74 | 75 | def oshpark_2layer(): 76 | '''Design rules for a OSHPark 2-layer board 77 | 78 | layer stackup: 79 | 1.0 mil (0.0254mm) solder resist +/-0.2mil (0.00508mm) 80 | 1.4 mil (0.0356mm) 1 oz copper 81 | 60 mil (1.5240mm) core +/-6.0mil (0.1524mm) 82 | 1.4 mil (0.0356mm) 1 oz copper 83 | 1.0 mil (0.0254mm) solder resist +/-0.2mil (0.00508mm) 84 | 85 | outline design rules: 86 | min_drill_size = 10mil (0.254mm) 87 | min_slot_width = 100" (1.7272mm) 88 | min_cutout_size = 100" (1.7272mm) 89 | 90 | net design rules: 91 | min_trace_width = 6mil (0.1524mm) 92 | min_clearance = 6mil (0.1524mm) 93 | min_via_drill = 10mil (0.254mm) 94 | min_annular_ring = 5mil (0.127mm) 95 | min_edge_clearance = 15mil (0.381mm) 96 | ''' 97 | 98 | layers = Layers([ 99 | Layer('silk_top', None, Materials.SilkScreen), 100 | Layer('finish_top', None, Materials.ENIG), 101 | Layer('mask_top', 0.0254, Materials.PurpleSolderMask), 102 | 103 | Layer('top', 0.0356, Materials.Cu), 104 | Layer('substrate', 1.5240, Materials.FR4), 105 | Layer('bottom', 0.0356, Materials.Cu), 106 | 107 | Layer('mask_bottom', 0.0254, Materials.PurpleSolderMask), 108 | Layer('finish_bottom', None, Materials.ENIG), 109 | Layer('silk_bottom', None, Materials.SilkScreen), 110 | ]) 111 | 112 | outline_design_rules = OutlineDesignRules( 113 | min_drill_size=0.254, 114 | min_slot_width=1.7272, 115 | min_cutout_size=1.7272 116 | ) 117 | 118 | trace_design_rules = TraceDesignRules( 119 | min_width=0.1524, 120 | min_clearance=0.1524, 121 | min_drill=0.254, 122 | min_annular_ring=0.127, 123 | min_edge_clearance=0.381, 124 | blind_vias_allowed=False, 125 | burried_vias_allowed=False 126 | ) 127 | 128 | cost_cm2 = 0.75 129 | return PcbAttributes(layers, outline_design_rules, 130 | trace_design_rules, cost_cm2) 131 | -------------------------------------------------------------------------------- /pycircuit/formats/yosys.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pycircuit.component import Pin 3 | from pycircuit.circuit import * 4 | from pycircuit.formats import extends 5 | 6 | 7 | def match_skin(inst): 8 | cname = inst.component.name 9 | value = '' if inst.value is None else inst.value 10 | suffix = '_h' if inst.horizontal else '_v' 11 | 12 | if cname == 'R' or cname == 'C' or cname == 'L': 13 | return cname.lower() + suffix 14 | 15 | if cname == 'D': 16 | if 'led' in value: 17 | return 'd_led' + suffix 18 | if 'sk' in value: 19 | return 'd_sk' + suffix 20 | return 'd' + suffix 21 | 22 | if cname == 'Q': 23 | if 'pnp' in value: 24 | return 'q_pnp' 25 | return 'q_npn' 26 | 27 | mapping = { 28 | 'OP': 'op', 29 | 'XTAL': 'xtal', 30 | 'V': 'v', 31 | 'I': 'i', 32 | 'Transformer_1P_1S': 'transformer_1p_1s' 33 | } 34 | 35 | if cname in mapping: 36 | return mapping[cname] 37 | 38 | return cname 39 | 40 | 41 | def match_port_direction(skin, assign): 42 | if skin.startswith('r_') or skin.startswith('c_') or skin.startswith('l_'): 43 | return {'A': 'input', 'B': 'output'}[assign.pin.name] 44 | if skin.startswith('d_'): 45 | return {'+': 'input', '-': 'output'}[assign.pin.name] 46 | if skin.startswith('q_'): 47 | return {'B': 'input', 'C': 'input', 'E': 'output'}[assign.pin.name] 48 | if skin == 'v' or skin == 'i': 49 | return {'+': 'input', '-': 'output'}[assign.pin.name] 50 | if skin == 'op': 51 | return {'VCC': 'input', 'VEE': 'output', '+': 'input', '-': 'input', 'OUT': 'output'}[assign.pin.name] 52 | if skin == 'xtal': 53 | return {'A': 'input', 'B': 'output'}[assign.pin.name] 54 | if skin == 'transformer_1p_1s': 55 | return {'L1.1': 'input', 'L1.2': 'input', 'L2.1': 'output', 'L2.2': 'output'}[assign.pin.name] 56 | if assign.erc_type == ERCType.INPUT: 57 | return 'input' 58 | elif assign.erc_type == ERCType.OUTPUT: 59 | return 'output' 60 | else: 61 | print('Warn: Pin %s has no direction!' % assign.pin.name) 62 | 63 | 64 | def gnd(conn): 65 | return { 66 | 'type': 'gnd', 67 | 'port_directions': { 68 | 'A': 'input' 69 | }, 70 | 'connections': { 71 | 'A': [conn] 72 | } 73 | } 74 | 75 | 76 | def vcc(conn): 77 | return { 78 | 'type': 'vcc', 79 | 'port_directions': { 80 | 'A': 'output' 81 | }, 82 | 'connections': { 83 | 'A': [conn] 84 | } 85 | } 86 | 87 | 88 | def vee(conn): 89 | return { 90 | 'type': 'vee', 91 | 'port_directions': { 92 | 'A': 'input' 93 | }, 94 | 'connections': { 95 | 'A': [conn] 96 | } 97 | } 98 | 99 | 100 | @extends(Circuit) 101 | def to_yosys(self): 102 | cells = {} 103 | for inst in self.insts: 104 | connections = {} 105 | port_directions = {} 106 | for assign in inst.assigns: 107 | uid = UID.uid() 108 | net_type = assign.net.type 109 | if net_type == NetType.GND: 110 | cells['gnd' + str(uid)] = gnd(uid) 111 | connections[assign.pin.name] = [uid] 112 | elif net_type == NetType.VCC: 113 | cells['vcc' + str(uid)] = vcc(uid) 114 | connections[assign.pin.name] = [uid] 115 | elif net_type == NetType.VEE: 116 | cells['vee' + str(uid)] = vee(uid) 117 | connections[assign.pin.name] = [uid] 118 | else: 119 | connections[assign.pin.name] = [assign.net.uid] 120 | 121 | skin = match_skin(inst) 122 | port_directions[assign.pin.name] = match_port_direction( 123 | skin, assign) 124 | 125 | cells[inst.name] = { 126 | 'type': match_skin(inst), 127 | 'port_directions': port_directions, 128 | 'connections': connections 129 | } 130 | 131 | ports = {} 132 | for port in self.external_ports(): 133 | if port.type == PortType.IN: 134 | direction = 'input' 135 | elif port.type == PortType.OUT: 136 | direction = 'output' 137 | else: 138 | continue 139 | 140 | ports[port.name] = { 141 | 'direction': direction, 142 | 'bits': [port.internal_net().uid] 143 | } 144 | 145 | return { 146 | 'modules': { 147 | self.name: { 148 | 'ports': ports, 149 | 'cells': cells 150 | } 151 | } 152 | } 153 | 154 | 155 | @extends(Circuit) 156 | def to_yosys_file(self, path): 157 | with open(path, 'w+') as f: 158 | f.write(json.dumps(self.to_yosys(), sort_keys=True, 159 | indent=2, separators=(',', ': '))) 160 | -------------------------------------------------------------------------------- /placer/box.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from z3 import * 3 | import math 4 | import uuid 5 | 6 | 7 | class Anchor(Enum): 8 | Min = 0 9 | Center = 1 10 | Max = 2 11 | 12 | 13 | class Coord(object): 14 | def __init__(self, anchor): 15 | self.anchor = anchor 16 | self.value = 0 17 | 18 | 19 | class Box(object): 20 | counter = 0 21 | symbols = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{};:\'",./<>?|\\' 22 | 23 | def __init__(self, inst, width, height, x_anchor=Anchor.Min, y_anchor=Anchor.Min): 24 | self.symbol = Box.symbols[Box.counter % len(Box.symbols)] 25 | Box.counter += 1 26 | 27 | self.inst = inst 28 | self.x = Coord(x_anchor) 29 | self.y = Coord(y_anchor) 30 | self.width = width 31 | self.height = height 32 | 33 | def set_position(self, x, y): 34 | self.x.value = x 35 | self.y.value = y 36 | return self 37 | 38 | def top(self): 39 | if self.y.anchor == Anchor.Max: 40 | return self.y.value 41 | if self.y.anchor == Anchor.Min: 42 | return self.y.value + self.height 43 | if self.y.anchor == Anchor.Center: 44 | return self.y.value + self.height / 2 45 | 46 | def bottom(self): 47 | if self.y.anchor == Anchor.Min: 48 | return self.y.value 49 | if self.y.anchor == Anchor.Max: 50 | return self.y.value - self.height 51 | if self.y.anchor == Anchor.Center: 52 | return self.y.value - self.height / 2 53 | 54 | def right(self): 55 | if self.x.anchor == Anchor.Max: 56 | return self.x.value 57 | if self.x.anchor == Anchor.Min: 58 | return self.x.value + self.width 59 | if self.x.anchor == Anchor.Center: 60 | return self.x.value + self.width / 2 61 | 62 | def left(self): 63 | if self.x.anchor == Anchor.Min: 64 | return self.x.value 65 | if self.x.anchor == Anchor.Max: 66 | return self.x.value - self.width 67 | if self.x.anchor == Anchor.Center: 68 | return self.x.value - self.width / 2 69 | 70 | def area(self): 71 | return self.height * self.width 72 | 73 | def place_inst(self, offset, grid_size): 74 | inst_attributes = self.inst.attributes 75 | x = (self.x.value + offset[0]) * grid_size 76 | y = (self.y.value + offset[1]) * grid_size 77 | inst_attributes.place(inst_attributes.layer, x, y) 78 | 79 | def __str__(self): 80 | return 'x=%s y=%s w=%s h=%s' % (self.x.value, self.y.value, 81 | self.height, self.width) 82 | 83 | @classmethod 84 | def from_inst(cls, inst): 85 | return cls(inst, inst.device.package.courtyard.ipc_width, 86 | inst.device.package.courtyard.ipc_height) 87 | 88 | 89 | class Z3Box(Box): 90 | 91 | def __init__(self, inst, width, height): 92 | super().__init__(inst, width, height, Anchor.Center, Anchor.Center) 93 | 94 | self.const_rx = int(math.ceil(width / 2)) 95 | self.const_ry = int(math.ceil(height / 2)) 96 | 97 | self.var_x = Int('%s_x' % str(inst.uid)) 98 | self.var_y = Int('%s_y' % str(inst.uid)) 99 | self.var_rot = Bool('%s_rot' % str(inst.uid)) 100 | self.var_rx = Int('%s_rx' % str(inst.uid)) 101 | self.var_ry = Int('%s_ry' % str(inst.uid)) 102 | 103 | def range_constraint(self, bin): 104 | return And(self.var_x >= self.var_rx, 105 | self.var_x <= bin.var_width - self.var_rx, 106 | self.var_y >= self.var_ry, 107 | self.var_y <= bin.var_height - self.var_ry) 108 | 109 | def rotation_constraint(self, allow_rotate=False): 110 | # Enabling rotation makes things much slower 111 | return And( 112 | BoolVal(True) if allow_rotate else self.var_rot == False, 113 | Implies(Not(self.var_rot), And(self.var_rx == self.const_rx, 114 | self.var_ry == self.const_ry)), 115 | Implies(self.var_rot, And(self.var_rx == self.const_ry, 116 | self.var_ry == self.const_rx)) 117 | ) 118 | 119 | def fix_position_constraint(self, x, y): 120 | return And(self.var_x == x, self.var_y == y) 121 | 122 | def overlap_constraint(self, other): 123 | return Or( 124 | self.var_x - other.var_x >= self.var_rx + other.var_rx, 125 | other.var_x - self.var_x >= self.var_rx + other.var_rx, 126 | self.var_y - other.var_y >= self.var_ry + other.var_ry, 127 | other.var_y - self.var_y >= self.var_ry + other.var_ry 128 | ) 129 | 130 | def set_rotation(self, rotation): 131 | width, height = self.width, self.height 132 | if rotation: 133 | self.width = height 134 | self.height = width 135 | 136 | def eval(self, model): 137 | self.set_position(model[self.var_x].as_long(), 138 | model[self.var_y].as_long()) 139 | self.set_rotation(is_true(model[self.var_rot])) 140 | -------------------------------------------------------------------------------- /pycircuit/pinassign.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from z3 import And, Or, Implies, Int, Distinct, Solver, sat 3 | 4 | 5 | class Z3Assign(object): 6 | def __init__(self, function, meta): 7 | self.function = function 8 | self.meta = meta 9 | 10 | self.constraints = [] 11 | 12 | _uuid = uuid.uuid4() 13 | self.z3_fun = Int(str(_uuid) + '_fun') 14 | self.z3_pin = Int(str(_uuid) + '_pin') 15 | self.z3_bus = Int(str(_uuid) + '_bus') 16 | 17 | def component_constraint(self, component): 18 | self.component = component 19 | 20 | fun_constraints = [] 21 | for fun in component.funs_by_function(self.function): 22 | assert(fun.id >= 0) 23 | assert(fun.bus_id is not None) 24 | 25 | # Constraint z3_fun to pin functions 26 | fun_constraints.append(self.z3_fun == fun.id) 27 | 28 | # z3_fun implies z3_pin and z3_bus 29 | self.constraints.append(Implies(self.z3_fun == fun.id, 30 | And(self.z3_bus == fun.bus_id, 31 | self.z3_pin == fun.pin.id))) 32 | self.constraints.append(Or(fun_constraints)) 33 | 34 | def eval(self, model): 35 | self.fun = self.component.funs[model[self.z3_fun].as_long()] 36 | self.pin = self.component.pins[model[self.z3_pin].as_long()] 37 | self.bus = model[self.z3_bus].as_long() 38 | 39 | def __iter__(self): 40 | yield self 41 | 42 | def __repr__(self): 43 | return self.function 44 | 45 | 46 | class Z3BusAssign(object): 47 | def __init__(self, *assigns): 48 | _uuid = uuid.uuid4() 49 | self.z3_bus = Int(str(_uuid) + '_bus') 50 | 51 | self.assigns = [] 52 | self.constraints = [] 53 | 54 | for assign in assigns: 55 | self.add_assign(assign) 56 | 57 | def add_assign(self, assign): 58 | # All assignments in a BusAssign need to have a the same bus 59 | self.assigns.append(assign) 60 | self.constraints.append(self.z3_bus == assign.z3_bus) 61 | 62 | def eval(self, model): 63 | self.bus = model[self.z3_bus].as_long() 64 | 65 | def __iter__(self): 66 | for assig in self.assigns: 67 | yield assig 68 | 69 | 70 | class AssignmentProblem(object): 71 | def __init__(self, component, assigns): 72 | self.component = component 73 | self.assigns = [] 74 | self.bus_assigns = [] 75 | self.constraints = [] 76 | 77 | for bus_assign in assigns: 78 | if isinstance(bus_assign, Z3Assign): 79 | bus_assign = Z3BusAssign(bus_assign) 80 | 81 | self.constraints += bus_assign.constraints 82 | self.bus_assigns.append(bus_assign) 83 | 84 | for assign in bus_assign: 85 | assign.component_constraint(component) 86 | self.constraints += assign.constraints 87 | self.assigns.append(assign) 88 | 89 | # Each assignment needs a different Fun 90 | self.constraints.append( 91 | Distinct([assign.z3_fun for assign in self.assigns])) 92 | 93 | # Each assignment needs a different Pin 94 | self.constraints.append( 95 | Distinct([assign.z3_pin for assign in self.assigns])) 96 | 97 | # Each BusFun needs a different bus 98 | self.constraints.append( 99 | Distinct([bus_assign.z3_bus for bus_assign in self.bus_assigns])) 100 | 101 | def solve(self): 102 | s = Solver() 103 | 104 | s.add(And(self.constraints)) 105 | 106 | if not s.check() == sat: 107 | print('Problem:') 108 | self.print_problem() 109 | print('Constraints:') 110 | for constraint in self.constraints: 111 | print(constraint) 112 | raise Exception('unsat') 113 | 114 | model = s.model() 115 | 116 | for assign in self.assigns: 117 | assign.eval(model) 118 | 119 | for bus_assign in self.bus_assigns: 120 | bus_assign.eval(model) 121 | 122 | def check_solution(self): 123 | pins = set() 124 | funs = set() 125 | busses = set() 126 | 127 | for assign in self.assigns: 128 | if assign.fun in funs: 129 | return 'Fun assigned multiple times' 130 | else: 131 | funs.add(assign.fun) 132 | 133 | if assign.pin in pins: 134 | return 'Pin assigned multiple times' 135 | else: 136 | pins.add(assign.pin) 137 | 138 | for bus_assign in self.bus_assigns: 139 | if bus_assign.bus in busses: 140 | return 'BusAssign needs unique bus' 141 | busses.add(bus_assign.bus) 142 | 143 | for assign in bus_assign: 144 | if not assign.bus == bus_assign.bus: 145 | return 'BusAssign Assigns need the same bus' 146 | 147 | def print_problem(self): 148 | print(self.assigns) 149 | 150 | def print_solution(self): 151 | for i, assign in enumerate(self.assigns): 152 | print('pin: %s func: %s assign: %s' % 153 | (assign.fun.pin.id, str(assign.fun), str(i))) 154 | -------------------------------------------------------------------------------- /pycircuit/formats/kicad.py: -------------------------------------------------------------------------------- 1 | import pykicad as ki 2 | from shapely.ops import polygonize 3 | from pycircuit.formats import extends, polygon_to_lines 4 | from pycircuit.outline import Outline 5 | from pycircuit.package import Package, Courtyard, Pad 6 | from pycircuit.pcb import Pcb, InstAttributes, NetAttributes 7 | from pycircuit.traces import Via, Segment 8 | 9 | 10 | @staticmethod 11 | @extends(Package) 12 | def from_kicad(kmodule): 13 | lines = [] 14 | for elem in kmodule.courtyard(): 15 | # Only Lines are supported in the courtyard at the moment 16 | assert isinstance(elem, ki.module.Line) 17 | start, end = (elem.start[0], elem.start[1]), (elem.end[0], elem.end[1]) 18 | lines.append((start, end)) 19 | 20 | polygons = list(polygonize(lines)) 21 | assert len(polygons) == 1 22 | coords = list(polygons[0].exterior.coords) 23 | 24 | package = Package(kmodule.name, Courtyard(coords), []) 25 | for kpad in kmodule.pads: 26 | drill = None if kpad.drill is None else kpad.drill.size 27 | package.add_pad(Pad(kpad.name, kpad.at[0], kpad.at[1], 28 | size=(kpad.size[0], kpad.size[1]), 29 | drill=drill, 30 | shape=kpad.shape)) 31 | return package 32 | 33 | 34 | @extends(Package) 35 | def to_kicad(package): 36 | kmod = ki.module.Module(package.name, layer='F.Cu', attr='smd') 37 | 38 | for start, end in polygon_to_lines(package.courtyard.coords): 39 | kline = ki.module.Line(list(start), list(end), layer='F.CrtYd', 40 | width=0.05) 41 | kmod.lines.append(kline) 42 | 43 | for pad in package.pads: 44 | pad_at = list(pad.location) 45 | pad_at[2] = pad.angle 46 | 47 | if pad.drill is None: 48 | pad_type = 'smd' 49 | pad_layers = ['F.Cu', 'F.Paste', 'F.Mask'] 50 | drill = None 51 | else: 52 | pad_type = 'thru_hole' 53 | pad_layers = ['*.Cu', 'F.Mask', 'B.Mask'] 54 | drill = ki.module.Drill(pad.drill) 55 | 56 | kpad = ki.module.Pad(pad.name, type=pad_type, shape=pad.shape, 57 | size=list(pad.size), at=pad_at, drill=drill, 58 | layers=pad_layers) 59 | kmod.pads.append(kpad) 60 | 61 | text_radius = package.size()[1] / 2 + 0.5 62 | ref = ki.module.Text(type='reference', layer='F.SilkS', thickness=0.15, 63 | size=[1, 1], text=package.name, 64 | at=[0, -text_radius - 0.35]) 65 | value = ki.module.Text(type='value', layer='F.Fab', thickness=0.15, 66 | size=[1, 1], text=package.name, 67 | at=[0, text_radius + 0.25]) 68 | kmod.texts = [ref, value] 69 | 70 | return kmod 71 | 72 | 73 | @extends(InstAttributes) 74 | def to_kicad(self): 75 | kmodule = self.inst.device.package.to_kicad() 76 | if self.layer.flip: 77 | kmodule.flip() 78 | if self.angle is not None: 79 | kmodule.rotate(self.angle) 80 | kmodule.place(self.x, self.y) 81 | kmodule.set_reference(self.inst.name) 82 | kmodule.set_value(self.inst.component.name) 83 | return kmodule 84 | 85 | 86 | @extends(Outline) 87 | def to_kicad(self, kpcb): 88 | for start, end in polygon_to_lines(list(self.polygon.exterior.coords)): 89 | kline = ki.pcb.GrLine(list(start), list( 90 | end), layer='Edge.Cuts', width=0.15) 91 | kpcb.lines.append(kline) 92 | for feature in self.features: 93 | center = [float(n) for n in feature.position] 94 | end = [float(n) 95 | for n in feature.position + [feature.drill_size / 2, 0]] 96 | kcircle = ki.pcb.GrCircle(list(center), list( 97 | end), layer='Edge.Cuts', width=0.15) 98 | kpcb.circles.append(kcircle) 99 | 100 | 101 | @extends(Pcb) 102 | def to_kicad(self): 103 | kpcb = ki.pcb.Pcb(title=self.netlist.name) 104 | 105 | # Add kicad modules 106 | for inst in self.netlist.insts: 107 | kpcb.modules.append(inst.attributes.to_kicad()) 108 | 109 | # Add kicad nets, segments and vias and connect modules 110 | for i, net in enumerate(self.netlist.nets): 111 | # Kicad nets need to have an id > 0 112 | knet_code = i + 1 113 | knet = ki.module.Net(net.name, knet_code) 114 | kpcb.nets.append(knet) 115 | 116 | for abspad in net.attributes.iter_pads(): 117 | kpcb.module_by_reference(abspad.inst.name) \ 118 | .connect(abspad.pad.name, knet) 119 | 120 | for seg in net.attributes.segments: 121 | layer = 'F.Cu' if seg.layer.layer.name == 'top' else 'B.Cu' 122 | kseg = ki.pcb.Segment(start=list(seg.start)[0:2], 123 | end=list(seg.end)[0:2], 124 | net=knet_code, width=seg.width, 125 | layer=layer) 126 | kpcb.segments.append(kseg) 127 | 128 | for via in net.attributes.vias: 129 | kvia = ki.pcb.Via(at=list(via.coord)[0:2], 130 | net=knet_code, 131 | size=self.diameter(), 132 | drill=self.drill()) 133 | kpcb.vias.append(kvia) 134 | 135 | self.outline.to_kicad(kpcb) 136 | return kpcb 137 | -------------------------------------------------------------------------------- /pycircuit/formats/spice.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import Inst, Netlist, UID 2 | from pycircuit.formats import extends 3 | 4 | 5 | class SpiceInst(object): 6 | def __init__(self, ty, ref, nodes, value=None, model=None): 7 | self.ty = ty 8 | self.ref = ref 9 | self.nodes = nodes 10 | self.value = value 11 | self.model = model 12 | 13 | if not ref.startswith(self.ty): 14 | self.ref = self.ty + self.ref 15 | 16 | def __str__(self): 17 | nodes = ' '.join([str(n) for n in self.nodes]) 18 | value = '' if self.value is None else str(self.value) 19 | model = '' if self.model is None else self.model.mname 20 | return '%s %s %s %s' % (self.ref, nodes, value, model) 21 | 22 | 23 | class SpiceModel(object): 24 | def __init__(self, ty, params): 25 | self.mname = 'mod' + str(UID.uid()) 26 | self.ty = ty 27 | self.params = params 28 | 29 | def __str__(self): 30 | params = ' '.join(['%s=%s' % (k, str(v)) 31 | for k, v in self.params.items()]) 32 | return '.model %s %s (%s)' % (self.mname, self.ty, params) 33 | 34 | 35 | class SpiceProbe(object): 36 | def __init__(self, ty, ref, node): 37 | self.ty = ty 38 | self.ref = ref 39 | self.node = node 40 | 41 | def __str__(self): 42 | return '.probe %s(%s) %s' % (self.ty, self.ref, str(self.node)) 43 | 44 | 45 | @extends(Inst) 46 | def to_spice(self): 47 | def nodes_from_assigns(*pin_names): 48 | nodes = [] 49 | for name in pin_names: 50 | if name == '0': 51 | nodes.append('0') 52 | else: 53 | assign = self.assign_by_pin_name(name) 54 | if assign is None: 55 | continue 56 | if assign.net.name.lower() == 'gnd': 57 | nodes.append('0') 58 | else: 59 | nodes.append(assign.net.name) 60 | return nodes 61 | 62 | models = [] 63 | 64 | if self.component.name == 'R': 65 | nodes = nodes_from_assigns('A', 'B') 66 | models.append(SpiceInst('R', self.name, nodes, self.value)) 67 | elif self.component.name == 'C': 68 | nodes = nodes_from_assigns('A', 'B') 69 | models.append(SpiceInst('C', self.name, nodes, self.value)) 70 | elif self.component.name == 'L': 71 | nodes = nodes_from_assigns('A', 'B') 72 | models.append(SpiceInst('L', self.name, nodes, self.value)) 73 | elif self.component.name == 'V': 74 | nodes = nodes_from_assigns('+', '-') 75 | net = 'n' + str(UID.uid()) 76 | models.append(SpiceInst('V', self.name, (net, nodes[1]), self.value)) 77 | models.append(SpiceInst('R', self.name, (net, nodes[0]), 30)) 78 | elif self.component.name == 'OP': 79 | nodes = nodes_from_assigns('OUT', '0', '+', '-') 80 | models.append(SpiceInst('E', self.name, nodes, 100000)) 81 | elif self.component.name == 'D': 82 | nodes = nodes_from_assigns('A', 'C') 83 | if 'led' in self.value.split(' '): 84 | model = SpiceModel('D', {'is': '1a', 'rs': 3.3, 'n': 1.8}) 85 | else: 86 | raise Exception('D value needs to contain led') 87 | models.append(model) 88 | models.append(SpiceInst('D', self.name, nodes, model=model)) 89 | elif self.component.name == 'Q': 90 | nodes = nodes_from_assigns('C', 'B', 'E', 'SUBSTRATE') 91 | if 'npn' in self.value.split(' '): 92 | model = SpiceModel('npn', {'is': 1e-15}) 93 | elif 'pnp' in self.value.split(' '): 94 | model = SpiceModel('pnp', {'is': 1e-15}) 95 | else: 96 | raise Exception('Q value needs to contain npn or pnp') 97 | models.append(model) 98 | models.append(SpiceInst('Q', self.name, nodes, model=model)) 99 | elif self.component.name == 'M': 100 | nodes = nodes_from_assigns('D', 'G', 'S', 'SUBSTRATE') 101 | if 'nmos' in self.value.split(' '): 102 | model = SpiceModel('nmos') 103 | elif 'pmos' in self.value.split(' '): 104 | model = SpiceModel('pmos') 105 | else: 106 | raise Exception('M value needs to contain nmos or pmos') 107 | models.append(model) 108 | models.append(SpiceModel('M', self.name, nodes, model=model)) 109 | elif self.component.name == 'Transformer_1P_1S': 110 | inductors = 'L1' + self.name, 'L2' + self.name 111 | nodes = nodes_from_assigns('L1.1', 'L1.2') 112 | models.append(SpiceInst('L', inductors[0], nodes, self.value)) 113 | nodes = nodes_from_assigns('L2.1', 'L2.2') 114 | models.append(SpiceInst('L', inductors[1], nodes, self.value)) 115 | models.append(SpiceInst('K', self.name, inductors, 1)) 116 | elif self.component.name == 'TP': 117 | node = nodes_from_assigns('TP') 118 | models.append(SpiceProbe('v', self.name, *node)) 119 | else: 120 | raise Exception("Component %s doesn't have a spice model" 121 | % self.component.name) 122 | 123 | return models 124 | 125 | 126 | @extends(Netlist) 127 | def to_spice(self, filename): 128 | with open(filename, 'w+') as f: 129 | # Emit title 130 | print('.title', filename, file=f) 131 | 132 | for inst in self.insts: 133 | for model in inst.to_spice(): 134 | print(str(model), file=f) 135 | 136 | # Emit end 137 | print('.end', file=f) 138 | -------------------------------------------------------------------------------- /pycircuit/outline.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from shapely.geometry import mapping, shape 3 | from shapely.geometry.polygon import Polygon 4 | 5 | 6 | class OutlineDesignRules(object): 7 | def __init__(self, min_drill_size, min_slot_width, min_cutout_size): 8 | self.min_drill_size = min_drill_size 9 | self.min_slot_width = min_slot_width 10 | self.min_cutout_size = min_cutout_size 11 | 12 | def to_object(self): 13 | return { 14 | 'min_drill_size': self.min_drill_size, 15 | 'min_slot_width': self.min_slot_width, 16 | 'min_cutout_size': self.min_cutout_size, 17 | } 18 | 19 | @classmethod 20 | def from_object(cls, obj): 21 | return cls(obj['min_drill_size'], obj['min_slot_width'], 22 | obj['min_cutout_size']) 23 | 24 | 25 | class OutlineDesignRuleError(Exception): 26 | pass 27 | 28 | 29 | class OutlineElement(object): 30 | def check(self, design_rules): 31 | assert isinstance(design_rules, OutlineDesignRules) 32 | raise NotImplementedError() 33 | 34 | 35 | class Hole(OutlineElement): 36 | def __init__(self, position, drill_size): 37 | self.position = position 38 | self.drill_size = drill_size 39 | 40 | def check(self, design_rules): 41 | if self.drill_size < design_rules.min_drill_size: 42 | raise OutlineDesignRuleError('Hole drill size needs to be larger than %d', 43 | design_rules.min_drill_size) 44 | 45 | def to_object(self): 46 | return { 47 | 'type': 'hole', 48 | 'drill_size': self.drill_size, 49 | 'x': float(self.position[0]), 50 | 'y': float(self.position[1]), 51 | } 52 | 53 | @classmethod 54 | def from_object(cls, obj): 55 | assert obj['type'] == 'hole' 56 | return cls(np.array([obj['x'], obj['y']]), obj['drill_size']) 57 | 58 | 59 | class Slot(OutlineElement): 60 | def __init__(self, position, drill_size, width, angle=0): 61 | self.position = position 62 | self.drill_size = drill_size 63 | self.width = width # hole to hole 64 | self.angle = angle 65 | 66 | def check(self, design_rules): 67 | if self.drill_size < design_rules.min_drill_size: 68 | raise OutlineDesignRuleError('Slot drill size needs to be larger than %d', 69 | design_rules.min_drill_size) 70 | if self.width < design_rules.min_slot_width + self.drill_size: 71 | raise OutlineDesignRuleError('Slot width needs to be larger than %d', 72 | design_rules.min_slot_width + self.drill_size) 73 | 74 | def to_object(self): 75 | return { 76 | 'type': 'slot', 77 | 'drill_size': self.drill_size, 78 | 'width': self.width, 79 | 'x': float(self.position[0]), 80 | 'y': float(self.position[1]), 81 | 'r': self.angle, 82 | } 83 | 84 | @classmethod 85 | def from_object(cls, obj): 86 | assert obj['type'] == 'slot' 87 | return cls(np.array(obj['x'], obj['y']), obj['drill_size'], 88 | obj['width'], obj['r']) 89 | 90 | 91 | class Cutout(OutlineElement): 92 | def __init__(self, polygon): 93 | assert isinstance(polygon, Polygon) 94 | self.polygon = polygon 95 | self.width = polygon.bounds[2] - polygon.bounds[0] 96 | self.height = polygon.bounds[3] - polygon.bounds[1] 97 | 98 | def check(self, design_rules): 99 | if self.height < design_rules.min_cutout_size: 100 | raise OutlineDesignRuleError('Cutout height must be larger than %d', 101 | design_rules.min_cutout_size) 102 | if self.width < design_rules.min_cutout_size: 103 | raise OutlineDesignRuleError('Cutout width must be larger than %d', 104 | design_rules.min_cutout_size) 105 | 106 | def to_object(self): 107 | return { 108 | 'type': 'cutout', 109 | 'polygon': mapping(self.polygon) 110 | } 111 | 112 | @classmethod 113 | def from_object(cls, obj): 114 | assert obj['type'] == 'cutout' 115 | return cls(shape(obj['polygon'])) 116 | 117 | 118 | class Outline(object): 119 | def __init__(self, polygon, *features): 120 | assert isinstance(polygon, Polygon) 121 | self.polygon = polygon 122 | self.features = [] 123 | 124 | for f in features: 125 | assert isinstance(f, OutlineElement) 126 | self.features.append(f) 127 | 128 | def __iter__(self): 129 | return iter(self.features) 130 | 131 | def check(self, design_rules): 132 | for f in self.features: 133 | f.check(design_rules) 134 | 135 | def to_object(self): 136 | return { 137 | 'polygon': mapping(self.polygon), 138 | 'features': [f.to_object() for f in self.features] 139 | } 140 | 141 | @classmethod 142 | def from_object(cls, obj): 143 | features = [] 144 | for f in obj['features']: 145 | if f['type'] == 'hole': 146 | features.append(Hole.from_object(f)) 147 | elif f['type'] == 'slot': 148 | features.append(Slot.from_object(f)) 149 | elif f['type'] == 'cutout': 150 | features.append(Cutout.from_object(f)) 151 | 152 | return cls(shape(obj['polygon']), *features) 153 | -------------------------------------------------------------------------------- /pycircuit/library/old/ti/tps6213x.py: -------------------------------------------------------------------------------- 1 | from pycircuit.device import * 2 | from pycircuit.footprint import * 3 | from pycircuit.circuit import * 4 | 5 | 6 | Device('TPS6213x', descr='''3V to 17V, 3A Step-Down Converter''', pins=[ 7 | PwrOut('SW', descr='''Switch node, which is connected to the internal 8 | MOSFET switches. Connect inductor between SW and output capacitor.'''), 9 | 10 | Out('PG', descr='''Output power good (High = Vout ready, Low = Vout below 11 | nominal regulation) ; open drain (requires pull-up resistor).'''), 12 | 13 | Pwr('AGND', descr='''Analog Ground. Must be connected directly to the 14 | Exposed Thermal Pad and common ground plane.'''), 15 | 16 | In('FB', descr='''Voltage feedback of adjustable version. Connect 17 | resistive voltage divider to this pin. It is recommended to connect FB 18 | to AGND on fixed output voltage versions for improved thermal 19 | performance.'''), 20 | 21 | In('FSW', descr='''Switching Frequency Slect (Low ~2.5MHz, High ~1.25MHz 22 | [0] for typical operation) [0] Connect FSW to Vout or PG in this case.'''), 23 | 24 | In('DEF', descr='''Output Voltage Scaling (Low = nominal, 25 | High = nominal +5%)'''), 26 | 27 | In('SS/TR', descr='''Soft-Start / Tracking Pin. An external capacitor 28 | connected to this pin sets the internal voltage reference rise time. It 29 | can be used for tracking and sequencing.'''), 30 | 31 | PwrIn('AVIN', descr='''Supply voltage for control circuitry. Connect to 32 | same source as PVIN.'''), 33 | 34 | PwrIn('PVIN', descr='''Supply voltage for power stage. Connect to same 35 | source as AVIN.'''), 36 | 37 | In('EN', descr='''Enable input (High = enabled, Low = disabled)'''), 38 | 39 | In('VOS', descr='''Output voltage sense pin and connection for the 40 | control loop circuitry.'''), 41 | 42 | Pwr('PGND', descr='''Power Ground. Must be connected directly to the 43 | Exposed Thermal Pad and common ground plane.'''), 44 | 45 | Pwr('EP', descr='''Must be connected to AGND, PGND and common ground 46 | plane. Must be soldered to achieve appropriate power dissipation and 47 | mechanical reliablility.''') 48 | ]) 49 | 50 | 51 | #Device('TPS62130', 'TPS62130x', adjustable=True) 52 | # Device('TPS62130A', 'TPS62130x', adjustable=True) # Power Good logic level low 53 | #Device('TPS62131', 'TPS62130x', vout=1.8, adjustable=False) 54 | #Device('TPS62132', 'TPS62130x', vout=3.3, adjustable=False) 55 | #Device('TPS62133', 'TPS62130x', vout=5, adjustable=False) 56 | 57 | 58 | Footprint('TPS6213x', 'TPS6213x', 'QFN16', # VQFN16 59 | Map(1, 'SW'), 60 | Map(2, 'SW'), 61 | Map(3, 'SW'), 62 | Map(4, 'PG'), 63 | Map(5, 'FB'), 64 | Map(6, 'AGND'), 65 | Map(7, 'FSW'), 66 | Map(8, 'DEF'), 67 | Map(9, 'SS/TR'), 68 | Map(10, 'AVIN'), 69 | Map(11, 'PVIN'), 70 | Map(12, 'PVIN'), 71 | Map(13, 'EN'), 72 | Map(14, 'VOS'), 73 | Map(15, 'PGND'), 74 | Map(16, 'PGND'), 75 | Map(17, 'EP')) 76 | 77 | 78 | @circuit('TPS6213x') 79 | def tps6213x(dev='TPS6213x', en=True, pg=False, fb=False): 80 | '''Returns a circuit for a TPS6213x Voltage Regulator. 81 | 82 | Keyword arguments: 83 | dev -- Device name 84 | en -- Connects EN to VIN when True (default True) 85 | pg -- Adds a pullup to PG or connects to GND when unused (default False) 86 | fb -- Feedback resistive voltage divider takes a tuple of resistor 87 | values (default False) 88 | ''' 89 | 90 | Node('U', dev) 91 | 92 | # Input circuitry. 93 | # PVIN and AVIN must be connected to same source 94 | Net('VIN') + Ref('U')['PVIN', 'AVIN'] 95 | # AGND, PGND and EP must be connected 96 | Net('GND') + Ref('U')['AGND', 'PGND', 'EP'] 97 | 98 | # Input decoupling capacitors 99 | Node('CIN1', 'C', '10uF') 100 | Node('CIN2', 'C', '0.1uF') 101 | Net('VIN') + Ref('CIN1')['~'] + Ref('CIN2')['~'] 102 | Net('GND') + Ref('CIN1')['~'] + Ref('CIN2')['~'] 103 | 104 | # Output circuitry. 105 | # Recommended inductor values are 1/2.2uH. 106 | Node('L', 'L', '2.2uH') 107 | Net('SW') + Ref('U')['SW'] + Ref('L')['~'] 108 | Net('VOUT') + Ref('L')['~'] + Ref('U')['VOS'] 109 | 110 | # Output decoupling capacitor. 111 | Node('COUT', 'C', '22uF') 112 | Nets('VOUT', 'GND') + Ref('COUT')['~', '~'] 113 | 114 | # Feedback voltage divider 115 | if fb: 116 | Node('FB_R1', 'R', fb[0]) 117 | Node('FB_R2', 'R', fb[1]) 118 | Net('VOUT') + Ref('FB_R1')['~'] 119 | Net('FB') + Ref('FB_R1')['~'] + Ref('U')['FB'] + Ref('FB_R2')['~'] 120 | Net('GND') + Ref('FB_R2')['~'] 121 | else: 122 | # Connect FB to GND for fixed output voltage regulators. 123 | Net('GND') + Ref('U')['FB'] 124 | 125 | # Misc configuration pins 126 | # EN pin 127 | if en: 128 | # Connect EN to VIN to enable device when power 129 | # is applied 130 | Net('VIN') + Ref('U')['EN'] 131 | 132 | # Power good pin 133 | if pg: 134 | # PG requires a pullup when used 135 | Node('PG_PULLUP', 'R', '100K') 136 | Net('PG') + Ref('U')['PG'] + Ref('PG_PULLUP')['~'] 137 | Net('VOUT') + Ref('PG_PULLUP')['~'] 138 | else: 139 | # PG should be connected to GND when unused 140 | Net('GND') + Ref('U')['PG'] 141 | 142 | # DEF and FSW pins are connected to GND by default 143 | Net('GND') + Ref('U')['DEF', 'FSW'] 144 | 145 | # SS/TR controls the startup time after EN goes high 146 | Node('C_START', 'C', '3.3nF') 147 | Net('SS/TR') + Ref('C_START')['~'] + Ref('U')['SS/TR'] 148 | Net('GND') + Ref('C_START')['~'] 149 | -------------------------------------------------------------------------------- /pycircuit/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | import shutil 4 | from pycircuit.circuit import Netlist, Circuit 5 | from pycircuit.compiler import Compiler 6 | from pycircuit.formats import * 7 | from pycircuit.pcb import Pcb 8 | 9 | 10 | def netlistsvg(filein, fileout): 11 | skin = '~/repos/netlistsvg/lib/analog.svg' 12 | os.system('netlistsvg --skin %s %s -o %s' % (skin, filein, fileout)) 13 | 14 | 15 | def default_compile(filein, fileout): 16 | compiler = Compiler() 17 | return compiler.compile(filein, fileout) 18 | 19 | 20 | def default_place(filein, fileout): 21 | Pcb.from_file(filein).to_file(fileout) 22 | 23 | 24 | def default_route(filein, fileout): 25 | Pcb.from_file(filein).to_file(fileout) 26 | 27 | 28 | def default_post_process(pcb, kpcb): 29 | return kpcb 30 | 31 | 32 | def string_to_filename(string): 33 | return string.lower().replace(' ', '_') 34 | 35 | 36 | class Builder(object): 37 | def __init__(self, circuit, 38 | outline=None, 39 | pcb_attributes=None, 40 | builddir='build', 41 | compile=default_compile, 42 | place=default_place, 43 | route=default_route, 44 | post_process=default_post_process): 45 | self.base_file_name = string_to_filename(circuit.name) 46 | self.builddir = builddir 47 | self.files = { 48 | 'hash': self.base_file_name + '.hash', 49 | 'net_in': self.base_file_name + '.net', 50 | 'net_out': self.base_file_name + '.out.net', 51 | 'place_in': self.base_file_name + '.place.pcb', 52 | 'place_out': self.base_file_name + '.place.out.pcb', 53 | 'route_in': self.base_file_name + '.place.out.pcb', 54 | 'route_out': self.base_file_name + '.route.out.pcb', 55 | 'spice': self.base_file_name + '.sp', 56 | 'net_yosys': self.base_file_name + '.json', 57 | 'net_svg': self.base_file_name + '.net.svg', 58 | 'pcb_svg': self.base_file_name + '.pcb.svg', 59 | 'kicad': self.base_file_name + '.kicad_pcb' 60 | } 61 | for tag, filename in self.files.items(): 62 | self.files[tag] = os.path.join(self.builddir, filename) 63 | 64 | self.hashs = {} 65 | self.circuit = circuit 66 | self.outline = outline 67 | self.pcb_attributes = pcb_attributes 68 | 69 | self.compile_hook = compile 70 | self.place_hook = place 71 | self.route_hook = route 72 | self.post_process_hook = post_process 73 | 74 | def file_hash(self, path): 75 | try: 76 | with open(path) as f: 77 | return hashlib.sha256(f.read().encode('utf-8')).hexdigest() 78 | except FileNotFoundError: 79 | return None 80 | 81 | def write_hashfile(self): 82 | with open(self.files['hash'], 'w+') as f: 83 | for name, path in self.files.items(): 84 | if name == 'hash': 85 | continue 86 | print(path, self.file_hash(path), file=f) 87 | 88 | def read_hashfile(self): 89 | try: 90 | with open(self.files['hash']) as f: 91 | for line in f.read().split('\n'): 92 | if line == '': 93 | continue 94 | path, digest = line.split(' ') 95 | if digest == 'None': 96 | digest = None 97 | self.hashs[path] = digest 98 | except FileNotFoundError: 99 | if not os.path.exists(self.builddir): 100 | os.makedirs(self.builddir) 101 | self.write_hashfile() 102 | self.read_hashfile() 103 | 104 | def stored_hash(self, name): 105 | return self.hashs[self.files[name]] 106 | 107 | def current_hash(self, name): 108 | return self.file_hash(self.files[name]) 109 | 110 | def load_pcb(self, place=False, route=False): 111 | file_name = self.files['place_in'] 112 | if place: 113 | file_name = self.files['place_out'] 114 | if route: 115 | file_name = self.files['route_out'] 116 | return Pcb.from_file(file_name) 117 | 118 | def step(self, input, output, call): 119 | if not self.stored_hash(input) == self.current_hash(input) \ 120 | or self.current_hash(output) is None: 121 | result = call(self.files[input], self.files[output]) 122 | self.write_hashfile() 123 | return True, result 124 | else: 125 | return False, None 126 | 127 | def build(self): 128 | self.compile() 129 | self.place() 130 | self.route() 131 | self.post_process() 132 | 133 | def compile(self): 134 | self.read_hashfile() 135 | self.circuit.to_file(self.files['net_in']) 136 | 137 | run, circuit = self.step('net_in', 'net_out', self.compile_hook) 138 | if not run: 139 | circuit = Circuit.from_file(self.files['net_out']) 140 | 141 | self.step('net_out', 'net_yosys', lambda _, 142 | x: circuit.to_yosys_file(x)) 143 | self.step('net_yosys', 'net_svg', netlistsvg) 144 | return circuit 145 | 146 | def place(self): 147 | netlist = Netlist.from_file(self.files['net_out']) 148 | pcb = Pcb(netlist, self.outline, self.pcb_attributes) 149 | 150 | self.step('net_out', 'place_in', lambda _, x: pcb.to_file(x)) 151 | self.step('place_in', 'place_out', self.place_hook) 152 | pcb = self.load_pcb(place=True) 153 | self.step('place_out', 'pcb_svg', lambda _, x: pcb.to_svg(x)) 154 | 155 | def route(self): 156 | pcb = self.load_pcb(place=True) 157 | self.step('place_out', 'route_in', lambda _, x: pcb.to_file(x)) 158 | self.step('route_in', 'route_out', self.route_hook) 159 | pcb = self.load_pcb(place=True, route=True) 160 | self.step('route_out', 'pcb_svg', lambda _, x: pcb.to_svg(x)) 161 | 162 | def post_process(self): 163 | pcb = self.load_pcb(place=True, route=True) 164 | kpcb = self.post_process_hook(pcb, pcb.to_kicad()) 165 | kpcb.to_file(self.files['kicad']) 166 | 167 | def clean(self): 168 | shutil.rmtree(self.builddir) 169 | -------------------------------------------------------------------------------- /pycircuit/component.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class PinType(Enum): 5 | POWER, GND, IN, OUT, INOUT, UNKNOWN = range(6) 6 | 7 | def __str__(self): 8 | return self.name.lower() 9 | 10 | @classmethod 11 | def from_string(cls, string): 12 | if string == 'power': 13 | return PinType.POWER 14 | if string == 'gnd': 15 | return PinType.GND 16 | if string == 'in': 17 | return PinType.IN 18 | if string == 'out': 19 | return PinType.OUT 20 | if string == 'inout': 21 | return PinType.INOUT 22 | if string == 'unknown': 23 | return PinType.UNKNOWN 24 | 25 | 26 | class Fun(object): 27 | def __init__(self, function, **kwargs): 28 | self.id = None 29 | self.bus_id = None 30 | self.pin = None 31 | 32 | self.function = function 33 | 34 | def __str__(self): 35 | return self.function 36 | 37 | def __repr__(self): 38 | return 'fn %s' % self.function 39 | 40 | 41 | class BusFun(Fun): 42 | def __init__(self, bus, function): 43 | super().__init__(function) 44 | 45 | self.bus = bus 46 | 47 | def __str__(self): 48 | return '%s %s' % (self.bus, self.function) 49 | 50 | def __repr__(self): 51 | return 'busfn %s %s' % (self.bus, self.function) 52 | 53 | 54 | class Pin(object): 55 | def __init__(self, name, *funs, **kwargs): 56 | self.id = None 57 | self.device = None 58 | self.name = name 59 | self.funs = [] 60 | self.type = PinType.UNKNOWN 61 | 62 | self.optional = True 63 | if 'optional' in kwargs: 64 | self.optional = kwargs['optional'] 65 | 66 | self.description = '' 67 | if 'description' in kwargs: 68 | self.description = kwargs['description'] 69 | 70 | if 'type' in kwargs: 71 | self.type = kwargs['type'] 72 | 73 | if len(funs) == 0: 74 | self.add_fun(Fun(self.name)) 75 | else: 76 | for fun in funs: 77 | self.add_fun(fun) 78 | 79 | def has_function(self, function): 80 | for fun in self.funs: 81 | if fun.function == function: 82 | return True 83 | return False 84 | 85 | def add_fun(self, fun): 86 | assert not self.has_function(fun.function) 87 | self.funs.append(fun) 88 | 89 | def __str__(self): 90 | return '%s %s' % (str(self.type), self.name) 91 | 92 | def __repr__(self): 93 | '''Return a string "Pin (Functions)".''' 94 | 95 | return '%-15s (%s)' % (str(self), ' | '.join([str(fun) for fun in self.funs])) 96 | 97 | 98 | class Component(object): 99 | components = [] 100 | 101 | def __init__(self, name, description, *pins): 102 | Component.components.append(self) 103 | 104 | self.name = name 105 | self.description = description 106 | 107 | self.pins = [] 108 | self.funs = [] 109 | self.busses = [] 110 | self._functions = set() 111 | 112 | for pin in pins: 113 | self.add_pin(pin) 114 | 115 | # Check that there is no Fun named like a BusFun 116 | # and no BusFun named like a Fun 117 | for function in self._functions: 118 | iterator = self.funs_by_function(function) 119 | ty = type(next(iterator)) 120 | for fun in iterator: 121 | assert type(fun) == ty 122 | 123 | def add_pin(self, pin): 124 | assert self.pin_by_name(pin.name) is None 125 | 126 | pin.id = len(self.pins) 127 | pin.component = self 128 | self.pins.append(pin) 129 | 130 | for fun in pin.funs: 131 | fun.id = len(self.funs) 132 | fun.pin = pin 133 | self.funs.append(fun) 134 | self._functions.add(fun.function) 135 | 136 | # Assign bus_id 137 | if isinstance(fun, BusFun): 138 | for i, bus in enumerate(self.busses): 139 | if fun.bus == bus: 140 | fun.bus_id = i 141 | break 142 | else: 143 | fun.bus_id = len(self.busses) 144 | self.busses.append(fun.bus) 145 | else: 146 | # make sure bus_id is unique and smaller zero 147 | fun.bus_id = -fun.id - 1 148 | 149 | def has_function(self, function): 150 | return function in self._functions 151 | 152 | def is_busfun(self, function): 153 | return isinstance(next(self.funs_by_function(function)), BusFun) 154 | 155 | def funs_by_function(self, function): 156 | for fun in self.funs: 157 | if fun.function == function: 158 | yield fun 159 | 160 | def pin_by_name(self, name): 161 | for pin in self.pins: 162 | if pin.name == name: 163 | return pin 164 | 165 | def __str__(self): 166 | '''Return the name of the component.''' 167 | 168 | return self.name 169 | 170 | def __repr__(self): 171 | component = '%s\n' % self.name 172 | for pin in self.pins: 173 | component += ' ' + repr(pin) + '\n' 174 | return component 175 | 176 | @classmethod 177 | def component_by_name(cls, name): 178 | '''Returns the Component called `name` from registered components.''' 179 | 180 | for c in cls.components: 181 | if c.name == name: 182 | return c 183 | 184 | raise IndexError('No Component with name ' + name) 185 | 186 | @classmethod 187 | def register_component(cls, component): 188 | '''Register a Component.''' 189 | 190 | try: 191 | cls.component_by_name(component.name) 192 | raise Exception('Component with name %s exists' % component.name) 193 | except IndexError: 194 | cls.components.append(component) 195 | 196 | 197 | class Io(Pin): 198 | def __init__(self, name, *funs, **kwargs): 199 | kwargs['type'] = PinType.INOUT 200 | super().__init__(name, Fun('GPIO'), *funs, **kwargs) 201 | 202 | 203 | class In(Pin): 204 | def __init__(self, name, *funs, **kwargs): 205 | kwargs['type'] = PinType.IN 206 | super().__init__(name, *funs, **kwargs) 207 | 208 | 209 | class Out(Pin): 210 | def __init__(self, name, *funs, **kwargs): 211 | kwargs['type'] = PinType.OUT 212 | super().__init__(name, *funs, **kwargs) 213 | 214 | 215 | class Pwr(Pin): 216 | def __init__(self, name, **kwargs): 217 | kwargs['type'] = PinType.POWER 218 | super().__init__(name, **kwargs) 219 | 220 | 221 | class Gnd(Pin): 222 | def __init__(self, name, **kwargs): 223 | kwargs['type'] = PinType.GND 224 | super().__init__(name, **kwargs) 225 | -------------------------------------------------------------------------------- /pycircuit/layers.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class BaseMaterial(Enum): 5 | def properties(self): 6 | return {} 7 | 8 | 9 | class Conductor(BaseMaterial): 10 | Cu, Ag, Au = list(range(3)) 11 | 12 | def properties(self): 13 | prop = {} 14 | 15 | _conductivity = { 16 | 'Cu': 5.96 * 10**7, # S/m @ 20C 17 | 'Ag': 6.30 * 10**7, # S/m @ 20C 18 | 'Au': 4.10 * 10**7, # S/m @ 20C 19 | } 20 | 21 | prop['conductivity'] = _conductivity[self.name] 22 | return prop 23 | 24 | 25 | class Dielectric(BaseMaterial): 26 | FR4, FR408, PVP = list(range(3)) 27 | 28 | def properties(self): 29 | prop = {} 30 | 31 | _permittivity = { 32 | 'PVP': 3, # Er ref: Guess 33 | 'FR4': 4.6, # Er @1MHz ref: OSHPark 34 | 'FR408': 3.66, # Er @1GHz ref: OSHPark 35 | } 36 | 37 | prop['permittivity'] = _permittivity[self.name] 38 | return prop 39 | 40 | 41 | class SolderMask(BaseMaterial): 42 | Purple, Red, Green, Blue, Black, White = list(range(6)) 43 | 44 | def properties(self): 45 | prop = {} 46 | 47 | _color = { 48 | 'Purple': 'purple', 49 | 'Red': 'red', 50 | 'Green': 'green', 51 | 'Blue': 'blue', 52 | 'Black': 'black', 53 | 'White': 'white', 54 | } 55 | 56 | prop['color'] = _color[self.name] 57 | return prop 58 | 59 | 60 | class SolderPaste(BaseMaterial): 61 | Any = list(range(1)) 62 | 63 | 64 | class Adhesive(BaseMaterial): 65 | Any = list(range(1)) 66 | 67 | 68 | class SilkScreen(BaseMaterial): 69 | Any = list(range(1)) 70 | 71 | 72 | class Finish(BaseMaterial): 73 | ENIG = list(range(1)) 74 | 75 | 76 | class MaterialType(Enum): 77 | Conductor, Dielectric, SolderPaste, Adhesive, SolderMask, SilkScreen, Finish \ 78 | = list(range(7)) 79 | 80 | def get_class(self): 81 | _class = { 82 | MaterialType.Conductor: Conductor, 83 | MaterialType.Dielectric: Dielectric, 84 | MaterialType.SolderMask: SolderMask, 85 | MaterialType.SolderPaste: SolderPaste, 86 | MaterialType.Adhesive: Adhesive, 87 | MaterialType.SilkScreen: SilkScreen, 88 | MaterialType.Finish: Finish, 89 | } 90 | 91 | return _class[self] 92 | 93 | 94 | class Material(object): 95 | def __init__(self, material_type, material=None): 96 | assert isinstance(material_type, MaterialType) 97 | self.material_type = material_type 98 | 99 | _class = material_type.get_class() 100 | if _class is not None: 101 | assert isinstance(material, _class) 102 | self.material = material 103 | 104 | self.is_conductor = False 105 | if material_type is MaterialType.Conductor: 106 | self.is_conductor = True 107 | 108 | def to_object(self): 109 | obj = { 110 | 'material_type': self.material_type.name 111 | } 112 | 113 | if self.material is not None: 114 | obj['material'] = self.material.name 115 | obj['properties'] = self.material.properties() 116 | 117 | return obj 118 | 119 | @classmethod 120 | def from_object(cls, obj): 121 | material_type = MaterialType[obj['material_type']] 122 | material = material_type.get_class()[obj['material']] 123 | return cls(material_type, material) 124 | 125 | 126 | class Materials(object): 127 | # Conductors 128 | Cu = Material(MaterialType.Conductor, Conductor.Cu) 129 | Ag = Material(MaterialType.Conductor, Conductor.Ag) 130 | Au = Material(MaterialType.Conductor, Conductor.Au) 131 | # Dielectrics 132 | FR4 = Material(MaterialType.Dielectric, Dielectric.FR4) 133 | FR408 = Material(MaterialType.Dielectric, Dielectric.FR408) 134 | PVP = Material(MaterialType.Dielectric, Dielectric.PVP) 135 | # Soldermasks 136 | PurpleSolderMask = Material(MaterialType.SolderMask, SolderMask.Purple) 137 | # Finishes 138 | ENIG = Material(MaterialType.Finish, Finish.ENIG) 139 | # Misc 140 | SolderPaste = Material(MaterialType.SolderPaste, SolderPaste.Any) 141 | SilkScreen = Material(MaterialType.SilkScreen, SilkScreen.Any) 142 | Adhesive = Material(MaterialType.Adhesive, Adhesive.Any) 143 | 144 | 145 | class Layer(object): 146 | def __init__(self, name, thickness, material): 147 | self.name = name 148 | self.thickness = thickness 149 | self.material = material 150 | 151 | def __str__(self): 152 | return self.name 153 | 154 | def __repr__(self): 155 | return self.name 156 | 157 | def to_object(self): 158 | return { 159 | 'name': self.name, 160 | 'thickness': self.thickness, 161 | 'material': self.material.to_object() 162 | } 163 | 164 | @classmethod 165 | def from_object(cls, obj): 166 | return cls(obj['name'], obj['thickness'], Material.from_object(obj['material'])) 167 | 168 | 169 | class RoutingLayer(object): 170 | def __init__(self, layer): 171 | self.layer = layer 172 | self.segments = [] 173 | self.vias = [] 174 | 175 | 176 | class PlacementLayer(object): 177 | def __init__(self, layer, flip=False): 178 | self.layer = layer 179 | self.flip = flip 180 | self.insts = [] 181 | 182 | 183 | class Layers(object): 184 | def __init__(self, layers): 185 | self.layers = layers 186 | 187 | self.placement_layers = [] 188 | self.routing_layers = [] 189 | 190 | # Add routing layers 191 | for layer in self.layers: 192 | if layer.material.is_conductor: 193 | self.routing_layers.append(RoutingLayer(layer)) 194 | 195 | # Add placement layers 196 | top = PlacementLayer(self.routing_layers[0].layer) 197 | bottom = PlacementLayer(self.routing_layers[-1].layer, flip=True) 198 | self.placement_layers += [top, bottom] 199 | 200 | def __getitem__(self, index): 201 | return self.layers[index] 202 | 203 | def __iter__(self): 204 | return iter(self.layers) 205 | 206 | def layer_by_name(self, name): 207 | for layer in self.layers: 208 | if layer.name == name: 209 | return layer 210 | 211 | def player_by_name(self, name): 212 | for player in self.placement_layers: 213 | if player.layer.name == name: 214 | return player 215 | 216 | def rlayer_by_name(self, name): 217 | for rlayer in self.routing_layers: 218 | if rlayer.layer.name == name: 219 | return rlayer 220 | 221 | def iter_conductive(self): 222 | for layer in self.layers: 223 | if layer.material.is_conductor: 224 | yield layer 225 | 226 | def __str__(self): 227 | return str([str(layer) for layer in self.layers]) 228 | 229 | def to_object(self): 230 | return [layer.to_object() for layer in self.layers] 231 | 232 | @classmethod 233 | def from_object(cls, obj): 234 | return cls([Layer.from_object(layer) for layer in obj]) 235 | -------------------------------------------------------------------------------- /pycircuit/traces.py: -------------------------------------------------------------------------------- 1 | from pycircuit.circuit import UID 2 | 3 | 4 | class TraceDesignRules(object): 5 | def __init__(self, min_width, min_clearance, 6 | min_annular_ring, min_drill, 7 | blind_vias_allowed, burried_vias_allowed, 8 | min_edge_clearance): 9 | self.min_width = min_width 10 | self.min_clearance = min_clearance 11 | self.min_annular_ring = min_annular_ring 12 | self.min_drill = min_drill 13 | self.blind_vias_allowed = blind_vias_allowed 14 | self.burried_vias_allowed = burried_vias_allowed 15 | self.min_edge_clearance = min_edge_clearance 16 | 17 | def to_netclass(self): 18 | return NetClass(segment_width=self.min_width, 19 | segment_clearance=self.min_clearance, 20 | via_drill=self.min_drill, 21 | via_diameter=self.min_annular_ring * 2 + self.min_drill, 22 | via_clearance=self.min_clearance, 23 | blind_vias=self.blind_vias_allowed, 24 | burried_vias=self.burried_vias_allowed) 25 | 26 | def to_object(self): 27 | return { 28 | 'min_width': self.min_width, 29 | 'min_clearance': self.min_clearance, 30 | 'min_annular_ring': self.min_annular_ring, 31 | 'min_drill': self.min_drill, 32 | 'blind_vias_allowed': self.blind_vias_allowed, 33 | 'burried_vias_allowed': self.burried_vias_allowed, 34 | 'min_edge_clearance': self.min_edge_clearance, 35 | } 36 | 37 | @classmethod 38 | def from_object(cls, obj): 39 | return cls(obj['min_width'], obj['min_clearance'], obj['min_annular_ring'], 40 | obj['min_drill'], obj['blind_vias_allowed'], 41 | obj['burried_vias_allowed'], obj['min_edge_clearance']) 42 | 43 | 44 | class TraceDesignRuleError(Exception): 45 | pass 46 | 47 | 48 | class NetClass(object): 49 | def __init__(self, net_class=None, segment_width=None, 50 | segment_clearance=None, via_diameter=None, 51 | via_drill=None, via_clearance=None, 52 | blind_vias=None, burried_vias=None, 53 | length_match=None, uid=None): 54 | self.uid = uid 55 | if uid is None: 56 | self.uid = UID.uid() 57 | self.parent = net_class 58 | self._segment_width = segment_width 59 | self._segment_clearance = segment_clearance 60 | self._via_diameter = via_diameter 61 | self._via_drill = via_drill 62 | self._via_clearance = via_clearance 63 | self._blind_vias = blind_vias 64 | self._burried_vias = burried_vias 65 | 66 | def __getattr__(self, attr): 67 | value = getattr(self, '_' + attr) 68 | if value is None: 69 | return getattr(self.parent, attr) 70 | return value 71 | 72 | def to_object(self): 73 | obj = {'uid': self.uid} 74 | if self.parent is not None: 75 | obj['net_class'] = self.parent.uid 76 | if self._segment_width is not None: 77 | obj['segment_width'] = self._segment_width 78 | if self._segment_clearance is not None: 79 | obj['segment_clearance'] = self._segment_clearance 80 | if self._via_diameter is not None: 81 | obj['via_diameter'] = self._via_diameter 82 | if self._via_drill is not None: 83 | obj['via_drill'] = self._via_drill 84 | if self._via_clearance is not None: 85 | obj['via_clearance'] = self._via_clearance 86 | if self._blind_vias is not None: 87 | obj['blind_vias'] = self._blind_vias 88 | if self._burried_vias is not None: 89 | obj['burried_vias'] = self._burried_vias 90 | return obj 91 | 92 | @classmethod 93 | def from_object(cls, obj, pcb): 94 | if 'net_class' in obj: 95 | obj['net_class'] = pcb.net_class_by_uid(obj['net_class']) 96 | return cls(**obj) 97 | 98 | 99 | class Segment(object): 100 | def __init__(self, net, start, end, routable_layer): 101 | self.net = net 102 | 103 | self.start = start 104 | self.end = end 105 | self.layer = routable_layer 106 | 107 | self.net.attributes.segments.append(self) 108 | self.layer.segments.append(self) 109 | 110 | def __getattr__(self, attr): 111 | if attr == 'width': 112 | return self.net.attributes.net_class.segment_width 113 | elif attr == 'length': 114 | return Point(self.start[0:2]).distance(Point(self.end[0:2])) 115 | 116 | def __str__(self): 117 | return '%s %s' % (str(self.start), str(self.end)) 118 | 119 | def check(self, design_rules): 120 | # TODO: check clearance and edge_clearance 121 | if self.width < design_rules.min_width: 122 | raise TraceDesignRuleError('Trace width needs to be larger than %d' 123 | % design_rules.min_width) 124 | 125 | def to_object(self): 126 | return { 127 | 'net': self.net.uid, 128 | 'start': self.start, 129 | 'end': self.end, 130 | 'layer': self.layer.layer.name, 131 | } 132 | 133 | @classmethod 134 | def from_object(cls, obj, pcb): 135 | net = pcb.netlist.net_by_uid(obj['net']) 136 | rlayer = pcb.attributes.layers.rlayer_by_name(obj['layer']) 137 | return cls(net, obj['start'], obj['end'], rlayer) 138 | 139 | 140 | class Via(object): 141 | def __init__(self, net, position, routable_layers): 142 | self.net = net 143 | 144 | self.position = position 145 | 146 | self.is_blind = False 147 | self.is_burried = False 148 | 149 | self.net.attributes.vias.append(self) 150 | for layer in layers: 151 | layer.vias.append(self) 152 | 153 | def __getattr__(self, attr): 154 | if attr == 'drill': 155 | return self.net.attributes.net_class.via_drill 156 | elif attr == 'diameter': 157 | return self.net.attributes.net_class.via_diameter 158 | 159 | def iter_layers(self): 160 | for layer in self.layers: 161 | yield layer 162 | 163 | def check(self, design_rules): 164 | # TODO: check clearance and edge_clearance 165 | if self.drill < design_rules.min_drill: 166 | raise TraceDesignRuleError('Via drill needs to be larger than %d' 167 | % design_rules.min_drill) 168 | min_diameter = design_rules.min_drill + 2 * design_rules.min_annular_ring 169 | if self.diameter < min_diameter: 170 | raise TraceDesignRuleError('Via diameter needs to be larger than %d' 171 | % min_diameter) 172 | if self.is_blind and not design_rules.blind_vias_allowed: 173 | raise TraceDesignRuleError('No blind vias allowed') 174 | if self.is_burried and not design_rules.burried_vias_allowed: 175 | raise TraceDesignRuleError('No burried vias allowed') 176 | 177 | def to_object(self): 178 | return { 179 | 'net': self.net.uid, 180 | 'x': float(self.position[0]), 181 | 'y': float(self.position[1]), 182 | 'layers': [layer.layer.name for layer in self.layers], 183 | } 184 | 185 | @classmethod 186 | def from_object(cls, obj, pcb): 187 | net = pcb.netlist.net_by_uid(obj['net']) 188 | net_class = pcb.net_class_by_uid(obj['net_class']) 189 | layers = [pcb.attributes.layers.rlayer_by_name(name) 190 | for layer in obj['layers']] 191 | return cls(net, net_class, np.array(obj['x'], obj['y']), rlayers) 192 | -------------------------------------------------------------------------------- /tests/test_circuit.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | from pycircuit.circuit import * 4 | 5 | 6 | class CircuitTests(unittest.TestCase): 7 | def setUp(self): 8 | self.circuit = Circuit('CircuitTests') 9 | Circuit.active_circuit = self.circuit 10 | 11 | def test_port(self): 12 | p1 = Port('p1', PortType.IN) 13 | assert self.circuit.port_by_name('p1') == p1 14 | assert self.circuit.port_by_uid(p1.uid) == p1 15 | assert str(p1) == 'p1' 16 | assert repr(p1) == 'in p1' 17 | 18 | def test_net(self): 19 | n1 = Net('n1') 20 | assert self.circuit.net_by_uid(n1.uid) == n1 21 | assert str(n1) == 'n1' 22 | assert repr(n1) == 'signal n1' 23 | 24 | def test_inst(self): 25 | r1 = Inst('R') 26 | assert self.circuit.inst_by_uid(r1.uid) == r1 27 | assert str(r1) == 'R' 28 | assert repr(r1) == 'inst R of R {\n}\n' 29 | 30 | def test_subinst(self): 31 | s1 = SubInst(Circuit('SubCircuit')) 32 | assert self.circuit.subinst_by_uid(s1.uid) == s1 33 | assert str(s1) == 'SubCircuit' 34 | assert repr(s1) == 'subinst SubCircuit of SubCircuit {\n}\n' 35 | 36 | def test_assign_inst_to_net(self): 37 | n1 = Net('n1') 38 | r1 = Inst('R') 39 | r1['~'] = n1 40 | assert len(r1.assigns) == 1 41 | assert len(n1.assigns) == 1 42 | assert repr(r1) == 'inst R of R {\n ~ = n1\n}\n' 43 | 44 | def test_assign_inst_to_port(self): 45 | p1 = Port('p1', PortType.IN) 46 | r1 = Inst('R') 47 | r1['~'] = p1 48 | assert len(r1.assigns) == 1 49 | assert len(p1.assigns) == 1 50 | assert repr(r1) == 'inst R of R {\n ~ = p1\n}\n' 51 | 52 | def test_assign_subinst_to_net(self): 53 | net = Net('n1') 54 | circuit = Circuit('SubCircuit') 55 | port_inner = Port('p', PortType.IN, _parent=circuit) 56 | subinst = SubInst(circuit) 57 | subinst['p'] = net 58 | assert len(subinst.assigns) == 1 59 | assert len(net.assigns) == 1 60 | assert repr( 61 | subinst) == 'subinst SubCircuit of SubCircuit {\n p = n1\n}\n' 62 | 63 | def test_assign_subinst_to_port(self): 64 | port = Port('p1', PortType.IN) 65 | circuit = Circuit('SubCircuit') 66 | port_inner = Port('p', PortType.IN, _parent=circuit) 67 | subinst = SubInst(circuit) 68 | subinst['p'] = port 69 | assert len(subinst.assigns) == 1 70 | assert len(port.assigns) == 1 71 | assert len(port_inner.assigns) == 1 72 | assert port.assigns[0].net == port_inner.assigns[0].net 73 | assert repr( 74 | subinst) == 'subinst SubCircuit of SubCircuit {\n p = p1\n}\n' 75 | 76 | def test_port_to_object(self): 77 | p1 = Port('p1', PortType.IN) 78 | obj = p1.to_object() 79 | assert isinstance(obj['uid'], int) 80 | assert isinstance(obj['guid'], int) 81 | assert obj['name'] == 'p1' 82 | 83 | def test_port_from_object(self): 84 | p1 = Port.from_object( 85 | {'uid': 0, 'guid': 1, 'name': 'p1', 'type': 0}, self.circuit) 86 | assert p1.uid == 0 87 | assert p1.name == 'p1' 88 | 89 | def test_net_to_object(self): 90 | n1 = Net('n1') 91 | obj = n1.to_object() 92 | assert isinstance(obj['uid'], int) 93 | assert isinstance(obj['guid'], int) 94 | assert obj['name'] == 'n1' 95 | 96 | def test_net_from_object(self): 97 | n1 = Port.from_object( 98 | {'uid': 0, 'guid': 1, 'name': 'n1', 'type': 0}, self.circuit) 99 | assert n1.uid == 0 100 | assert n1.name == 'n1' 101 | 102 | def test_inst_to_object(self): 103 | r1 = Inst('R') 104 | obj = r1.to_object() 105 | assert isinstance(obj['uid'], int) 106 | assert isinstance(obj['guid'], int) 107 | assert obj['name'] == 'R' 108 | assert obj['component'] == 'R' 109 | assert obj['value'] == None 110 | assert obj['assigns'] == [] 111 | 112 | def test_inst_from_object(self): 113 | r1 = Inst.from_object({ 114 | 'uid': 0, 115 | 'guid': 1, 116 | 'name': 'R1', 117 | 'component': 'R', 118 | 'value': None, 119 | 'assigns': [] 120 | }, self.circuit) 121 | assert r1.uid == 0 122 | assert r1.name == 'R1' 123 | assert isinstance(r1.component, Component) 124 | assert r1.component.name == 'R' 125 | assert r1.device is None 126 | assert r1.value is None 127 | 128 | def test_circuit_to_object(self): 129 | p1, n1, r1 = Port('p1', PortType.IN), Net('n1'), Inst('R') 130 | obj = self.circuit.to_object() 131 | assert obj['name'] == 'CircuitTests' 132 | assert len(obj['ports']) == 1 133 | assert len(obj['nets']) == 1 134 | assert len(obj['insts']) == 1 135 | 136 | def test_circuit_from_object(self): 137 | c1 = Circuit.from_object({ 138 | 'name': 'SubCircuit', 139 | 'ports': [{'uid': 0, 'guid': 2, 'name': 'p1', 'type': 0}], 140 | 'nets': [{'uid': 1, 'guid': 3, 'name': 'n1'}], 141 | 'insts': [{ 142 | 'uid': 4, 143 | 'guid': 5, 144 | 'name': 'R1', 145 | 'component': 'R', 146 | 'value': None, 147 | 'assigns': [] 148 | }], 149 | }, None) 150 | assert c1.name == 'SubCircuit' 151 | assert len(c1.ports) == 1 152 | assert isinstance(c1.ports[0], Port) 153 | assert len(c1.nets) == 1 154 | assert isinstance(c1.nets[0], Net) 155 | assert len(c1.insts) == 1 156 | assert isinstance(c1.insts[0], Inst) 157 | 158 | def test_inst_assign_to_object(self): 159 | inst = Inst('R') 160 | function = '~' 161 | net = Net('n1') 162 | assign = InstAssign(inst, function, net) 163 | obj = assign.to_object() 164 | assert isinstance(obj['uid'], int) 165 | assert isinstance(obj['guid'], int) 166 | assert obj['function'] == function 167 | assert obj['net'] == net.uid 168 | 169 | def test_inst_assign_from_object(self): 170 | uid = UID.uid() 171 | guid = UID.uid() 172 | inst = Inst('R') 173 | function = '~' 174 | net = Net('n1') 175 | assign = InstAssign.from_object({ 176 | 'uid': uid, 177 | 'guid': guid, 178 | 'function': function, 179 | 'pin': None, 180 | 'type': None, 181 | 'net': net.uid, 182 | }, self.circuit, inst) 183 | assert assign.uid == uid 184 | assert assign.guid == guid 185 | assert assign.inst == inst 186 | assert assign.function == function 187 | assert assign.pin is None 188 | assert assign.type is None 189 | assert assign.net == net 190 | 191 | def test_port_assign_to_object(self): 192 | circuit = Circuit('sub') 193 | port = Port('p1', PortType.IN, _parent=circuit) 194 | subinst = SubInst(circuit) 195 | net = Net('n1') 196 | assign = PortAssign(port, net) 197 | obj = assign.to_object() 198 | assert isinstance(obj['uid'], int) 199 | assert isinstance(obj['guid'], int) 200 | assert obj['net'] == net.uid 201 | 202 | def test_port_assign_from_object(self): 203 | uid = UID.uid() 204 | guid = UID.uid() 205 | circuit = Circuit('sub') 206 | port = Port('p1', PortType.IN, _parent=circuit) 207 | subinst = SubInst(circuit) 208 | net = Net('n1') 209 | assign = PortAssign.from_object({ 210 | 'uid': uid, 211 | 'guid': guid, 212 | 'net': net.uid, 213 | }, self.circuit, port) 214 | assert assign.uid == uid 215 | assert assign.guid == guid 216 | assert assign.port == port 217 | assert assign.net == net 218 | -------------------------------------------------------------------------------- /router/router.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from shapely.geometry import Point, LineString, MultiLineString 3 | from shapely.ops import linemerge 4 | from pycircuit.pcb import Pcb 5 | from pycircuit.traces import Segment 6 | from router.monosat import route_multi 7 | 8 | 9 | class Router(object): 10 | def __init__(self, grid_size=0.05, allow_45=False, 11 | monosat_args=[], amo_builtin_size=20, heuristic_edge_weights=0, 12 | graph_separation_enforcement=2, maxflow_enforcement_level=0, 13 | flowgraph_separation_enforcement=0): 14 | '''Initialize Router with settings. 15 | 16 | Keyword arguments: 17 | amo-builtin-size -- (default 20) 18 | The largest at-most-one constraint size to manually build instead of 19 | using builtin AMO solver. 20 | 21 | heuristic-edge-weights -- (default 0) (choices range(0, 2)) 22 | This enables a heuristic which sets assigned edges to unit weight, 23 | to encourage edge-reuse in solutions in the solver. 24 | 25 | graph-separation-enforcement -- (default 2) (choices range(1, 4)) 26 | This controls the type of constraint that prevents nets from 27 | intersecting. All three are reasonable choices. 28 | 29 | maxflow-enforcement-level -- (default 0) (choices range(0, 5)) 30 | Set to >= 1 to enable redundant, over-approximative maximum flow 31 | constraints, which can help the solver prune bad solutions early. 32 | Options 2, 3, 4 control heuristic interactions between the flow 33 | constraints and the routing constraints in the solver. 34 | 35 | flowgraph-separation-enforcement -- (default 0) (choices range(0, 4)) 36 | This controls the type of constraint used to prevent nets from 37 | intersecting with each other in the maximum flow constraint, IF 38 | maxflow constraints are used. 39 | ''' 40 | 41 | self.grid_size = grid_size 42 | self.allow_45 = allow_45 43 | 44 | # default argument for MonoSAT; enables the heuristics described in 45 | # "Routing Under Constraints", FMCAD 2016, A. Nadel 46 | monosat_args.append('-ruc') 47 | 48 | assert heuristic_edge_weights in range(0, 2) 49 | assert graph_separation_enforcement in range(1, 4) 50 | assert maxflow_enforcement_level in range(0, 5) 51 | assert flowgraph_separation_enforcement in range(0, 4) 52 | 53 | self.monosat_args = monosat_args 54 | self.amo_builtin_size = amo_builtin_size 55 | self.heuristic_edge_weights = heuristic_edge_weights 56 | self.graph_separation_enforcement = graph_separation_enforcement 57 | self.maxflow_enforcement_level = maxflow_enforcement_level 58 | self.flowgraph_separation_enforcement = flowgraph_separation_enforcement 59 | 60 | def route(self, filein, fileout): 61 | pcb = Pcb.from_file(filein) 62 | 63 | width, height, bounds = *pcb.size(), pcb.outline.polygon.exterior.bounds 64 | grid_width, grid_height = int(width / self.grid_size + 1), \ 65 | int(height / self.grid_size + 1) 66 | 67 | def pos_to_grid(x, y): 68 | return ( 69 | int((x - bounds[0]) / self.grid_size), 70 | int((y - bounds[1]) / self.grid_size) 71 | ) 72 | 73 | def grid_to_pos(x, y): 74 | return ( 75 | x * self.grid_size + bounds[0], 76 | y * self.grid_size + bounds[1] 77 | ) 78 | 79 | nets = [] 80 | for net in pcb.netlist.nets: 81 | nets.append([]) 82 | for pad in net.attributes.iter_pads(): 83 | nets[-1].append(pos_to_grid(*pad.location[0:2])) 84 | # for inst in self.netlist.insts: 85 | # for pad in inst.attributes.iter_pads(): 86 | # loc = grid(pad.location[0] - pad.size[0] / 2, 87 | # pad.location[1] - pad.size[1] / 2) 88 | # width = int(math.ceil(pad.size[0] / grid_size)) 89 | # height = int(math.ceil(pad.size[1] / grid_size)) 90 | # vids = [] 91 | # for x in range(width): 92 | # for y in range(height): 93 | # vids.append(vid(x + loc[0], y + loc[1])) 94 | #print('C', ' '.join(vids), file=f) 95 | 96 | ## ROUTE ## 97 | nets = route_multi(self, grid_width, grid_height, nets) 98 | 99 | def reduce_linestring(linestring): 100 | if len(linestring.coords) < 3: 101 | return linestring 102 | coords = [linestring.coords[0]] 103 | for i, end in enumerate(linestring.coords[2:]): 104 | start = linestring.coords[i] 105 | test = linestring.coords[i + 1] 106 | if not LineString([start, end]).contains(Point(test)): 107 | coords.append(test) 108 | coords.append(linestring.coords[-1]) 109 | return LineString(coords) 110 | 111 | net_segments = [] 112 | for i, coords in enumerate(nets): 113 | net_segments.append([]) 114 | 115 | lines = [] 116 | for a, b in itertools.combinations(coords, 2): 117 | if Point(a).distance(Point(b)) == 1: 118 | lines.append(LineString([a, b])) 119 | mls = linemerge(MultiLineString(lines)) 120 | if not isinstance(mls, MultiLineString): 121 | line = reduce_linestring(mls).coords 122 | net_segments[-1].append(line) 123 | else: 124 | for line in mls: 125 | line = reduce_linestring(line).coords 126 | net_segments[-1].append(line) 127 | 128 | for segments, net in zip(net_segments, pcb.netlist.nets): 129 | for seg in segments: 130 | for i, coord in enumerate(seg[1:]): 131 | start = grid_to_pos(*seg[i]) 132 | end = grid_to_pos(*coord) 133 | Segment(net, start, end, 134 | pcb.attributes.layers.routing_layers[0]) 135 | 136 | pcb.to_file(fileout) 137 | 138 | 139 | if __name__ == '__main__': 140 | import sys 141 | 142 | monosat_args = ['-ruc'] 143 | 144 | parser = argparse.ArgumentParser( 145 | description='SAT-based, constrained multi-terminal routing') 146 | 147 | parser.add_argument('--amo-builtin-size', default=20, type=int, help='''The largest at-most-one constraint size to manually 148 | build instead of using builtin AMO solver''') 149 | parser.add_argument('--heuristic-edge-weights', default=0, type=int, 150 | choices=range(0, 2), help='''This enables a heuristic 151 | which sets assigned edges to unit weight, to encourage 152 | edge-reuse in solutions in the solver.''') 153 | parser.add_argument('--graph-separation-enforcement', default=2, type=int, 154 | choices=range(1, 4), help='''This controls the type of 155 | constraint that prevents nets from intersecting. All 156 | three are reasonable choices.''') 157 | parser.add_argument('--maxflow-enforcement-level', default=0, type=int, 158 | choices=range(0, 5), help='''Set to >= 1 to enable 159 | redundant, over-approximative maximum flow constraints, 160 | which can help the solver prune bad solutions early. 161 | Options 2,3,4 control heuristic interactions between the 162 | flow constraints and the routing constraints in the 163 | solver.''') 164 | parser.add_argument('--flowgraph-separation-enforcement', default=0, type=int, 165 | choices=range(0, 4), help='''This controls the type of 166 | constraint used to prevent nets from intersecting with 167 | each other in the maximum flow constraint, IF maxflow 168 | constraints are used.''') 169 | 170 | parser.add_argument('filein', type=str) 171 | parser.add_argument('fileout', type=str) 172 | 173 | args, unknown = parser.parse_known_args() 174 | 175 | if len(unknown) > 0: 176 | print("Passing unrecognized arguments to monosat: " + str(unknown)) 177 | monosat_args = unknown 178 | 179 | router = Router(monosat_args, 180 | amo_builtin_size=amo_builtin_size, 181 | heuristic_edge_weights=heuristic_edge_weights, 182 | graph_separation_enforcement=graph_separation_enforcement, 183 | maxflow_enforcement_level=maxflow_enforcement_level, 184 | flowgraph_separation_enforcement=flowgraph_separation_enforcement) 185 | 186 | router.route(args.filein, args.fileout) 187 | -------------------------------------------------------------------------------- /pycircuit/library/old/sifive/fe310g000.py: -------------------------------------------------------------------------------- 1 | from pycircuit.device import * 2 | from pycircuit.footprint import * 3 | from pycircuit.circuit import * 4 | from pycircuit.library import * 5 | 6 | 7 | Device('FE310-G000', pins=[ 8 | Pwr('GND', descr='0V Ground input'), 9 | PwrIn('VDD', descr='+1.8V Core voltage supply input'), 10 | PwrIn('IVDD', descr='+3.3V I/O voltage supply input'), 11 | PwrIn('AON_VDD', descr='+1.8V Always-On core voltage supply input'), 12 | PwrIn('AON_IVDD', descr='+1.8V Always-On I/O voltage supply input'), 13 | PwrIn('PLL_AVDD', descr='+1.8V PLL Supply input'), 14 | Pwr('PLL_AVSS', descr='''PLL VSS input. Connect through a capacitor to 15 | PLL_AVDD, not to GND'''), 16 | PwrIn('OTP_AIVDD', descr='+3.3V OTP Supply Input'), 17 | 18 | Pin('XTAL_XI', descr='16MHz Crystal Oscillator Input'), 19 | Pin('XTAL_XO', descr='16MHz Crystal Oscillator Output'), 20 | 21 | Pin('JTAG_TCK', Fun('JTAG:S', 'TCK')), 22 | Pin('JTAG_TDO', Fun('JTAG:S', 'TDO')), 23 | Pin('JTAG_TMS', Fun('JTAG:S', 'TMS')), 24 | Pin('JTAG_TDI', Fun('JTAG:S', 'TDI')), 25 | 26 | Pin('QSPI_DQ_3', Fun('QSPI:M', 'DQ3')), 27 | Pin('QSPI_DQ_2', Fun('QSPI:M', 'DQ2')), 28 | Pin('QSPI_DQ_1', Fun('QSPI:M', 'DQ1')), 29 | Pin('QSPI_DQ_0', Fun('QSPI:M', 'DQ0')), 30 | Pin('QSPI_CS', Fun('QSPI:M', 'SS0')), 31 | Pin('QSPI_SCK', Fun('QSPI:M', 'SCLK')), 32 | 33 | Pin('GPIO_0', 'GPIO', Fun('PWM0', 'PWM', '0')), 34 | Pin('GPIO_1', 'GPIO', Fun('PWM0', 'PWM', '1')), 35 | Pin('GPIO_2', 'GPIO', Fun('PWM0', 'PWM', '2'), Fun('SPI1', 'SPI:M', 'SS0')), 36 | Pin('GPIO_3', 'GPIO', Fun('PWM0', 'PWM', '3'), Fun('SPI1', 'SPI:M', 'MOSI')), 37 | Pin('GPIO_4', 'GPIO', Fun('SPI1', 'SPI:M', 'MISO')), 38 | Pin('GPIO_5', 'GPIO', Fun('SPI1', 'SPI:M', 'SCLK')), 39 | Pin('GPIO_9', 'GPIO', Fun('SPI1', 'SPI:M', 'SS1')), 40 | Pin('GPIO_10', 'GPIO', Fun('PWM2', 'PWM', '0'), Fun('SPI1', 'SPI:M', 'SS2')), 41 | Pin('GPIO_11', 'GPIO', Fun('PWM2', 'PWM', '1')), 42 | Pin('GPIO_12', 'GPIO', Fun('PWM2', 'PWM', '2')), 43 | Pin('GPIO_13', 'GPIO', Fun('PWM2', 'PWM', '3')), 44 | Pin('GPIO_16', 'GPIO', Fun('UART0', 'UART', 'RX')), 45 | Pin('GPIO_17', 'GPIO', Fun('UART0', 'UART', 'TX')), 46 | Pin('GPIO_18', 'GPIO'), 47 | Pin('GPIO_19', 'GPIO', Fun('PWM1', 'PWM', '1')), 48 | Pin('GPIO_20', 'GPIO', Fun('PWM1', 'PWM', '0')), 49 | Pin('GPIO_21', 'GPIO', Fun('PWM1', 'PWM', '2')), 50 | Pin('GPIO_22', 'GPIO', Fun('PWM1', 'PWM', '3')), 51 | Pin('GPIO_23', 'GPIO'), 52 | 53 | Out('AON_PMU_OUT_1', # domain='aon', 54 | descr='Programmable SLEEP control'), 55 | 56 | Out('AON_PMU_OUT_0', # domain='aon' 57 | descr='Programmable SLEEP control'), 58 | 59 | In('AON_PMU_DWAKEUP_N', # domain='aon', 60 | descr='Digital Wake-from-sleep. Active low.'), 61 | 62 | In('AON_ERST_N', # domain='aon', 63 | descr='External System Reset. Active low.'), 64 | 65 | In('AON_PSD_LFALTCLK', # domain='aon', 66 | descr='Optional 32kHz Clock input'), 67 | 68 | In('AON_PSD_LFCLKSEL', # domain='aon', 69 | descr='''32kHz clock source selector. When 70 | driven low, AON_PSD_LFALTCLK input is used as the 32kHz low-frequency 71 | clock source. When left unconnected or driven high, the internal LFROSC 72 | source is used.''') 73 | ]) 74 | 75 | 76 | Footprint('FE310-G000', 'FE310-G000', 'QFN48', 77 | Map(1, 'QSPI_DQ_3'), 78 | Map(2, 'QSPI_DQ_2'), 79 | Map(3, 'QSPI_DQ_1'), 80 | Map(4, 'QSPI_DQ_0'), 81 | Map(5, 'QSPI_CS'), 82 | Map(6, 'VDD'), 83 | Map(7, 'PLL_AVDD'), 84 | Map(8, 'PLL_AVSS'), 85 | Map(9, 'XTAL_XI'), 86 | Map(10, 'XTAL_XO'), 87 | Map(11, 'IVDD'), 88 | Map(12, 'OTP_AIVDD'), 89 | Map(13, 'JTAG_TCK'), 90 | Map(14, 'JTAG_TDO'), 91 | Map(15, 'JTAG_TMS'), 92 | Map(16, 'JTAG_TDI'), 93 | Map(17, 'AON_PMU_OUT_1'), 94 | Map(18, 'AON_PMU_OUT_0'), 95 | Map(19, 'AON_IVDD'), 96 | Map(20, 'AON_PSD_LFALTCLK'), 97 | Map(21, 'AON_PSD_LFCLKSEL'), 98 | Map(22, 'AON_PMU_DWAKEUP_N'), 99 | Map(23, 'AON_VDD'), 100 | Map(24, 'AON_ERST_N'), 101 | Map(25, 'GPIO_0'), 102 | Map(26, 'GPIO_1'), 103 | Map(27, 'GPIO_2'), 104 | Map(28, 'GPIO_3'), 105 | Map(29, 'GPIO_4'), 106 | Map(30, 'VDD'), 107 | Map(31, 'GPIO_5'), 108 | Map(32, 'IVDD'), 109 | Map(33, 'GPIO_9'), 110 | Map(34, 'GPIO_10'), 111 | Map(35, 'GPIO_11'), 112 | Map(36, 'GPIO_12'), 113 | Map(37, 'GPIO_13'), 114 | Map(38, 'GPIO_16'), 115 | Map(39, 'GPIO_17'), 116 | Map(40, 'GPIO_18'), 117 | Map(41, 'GPIO_19'), 118 | Map(42, 'GPIO_20'), 119 | Map(43, 'GPIO_21'), 120 | Map(44, 'GPIO_22'), 121 | Map(45, 'GPIO_23'), 122 | Map(46, 'VDD'), 123 | Map(47, 'IVDD'), 124 | Map(48, 'QSPI_SCK'), 125 | Map(49, 'GND')) 126 | 127 | 128 | @circuit('AON-BTN') 129 | def aon_btn(name, debounce=False): 130 | Sub(name + '_BTN', button()) 131 | Node(name + '_D', 'D') 132 | Node(name + '_R', 'R', '100K') 133 | 134 | Net('VDD_1V8') + Ref(name + '_R')['~'] 135 | Net('IN') + Ref(name + '_D')['A'] + Ref(name + '_R')['~'] 136 | Net(name) + Ref(name + '_D')['K'] + Ref(name + '_BTN')['IN'] 137 | Net('VDD_3V3') + Ref(name + '_BTN')['VDD'] 138 | Net('GND') + Ref(name + '_BTN')['GND'] 139 | 140 | if debounce: 141 | Node(name + '_C', 'C', '10nF') 142 | # Shorts RESET_R on power glitch to instantaneously 143 | # discharge RESET_C 144 | Node(name + '_C_D', 'D') 145 | Net('VDD_1V8') + Ref(name + '_C_D')['K'] 146 | Net('IN') + Ref(name + '_C')['~'] + Ref(name + '_C_D')['A'] 147 | Net('GND') + Ref(name + '_C')['~'] 148 | 149 | 150 | @circuit('FE310-G000') 151 | def fe310g000(qspi_flash=True, lfaltclk=True): 152 | '''Returns a circuit for a FE310-G000 Microcontroller.''' 153 | 154 | Node('U', 'FE310-G000') 155 | 156 | Net('VDD_1V8') + Ref('U')['VDD', 'AON_VDD', 'AON_IVDD'] 157 | Net('VDD_3V3') + Ref('U')['IVDD', 'OTP_AIVDD'] 158 | Net('GND') + Ref('U')['GND'] 159 | 160 | # VDD decoupling caps 161 | Sub('VDD_C', decoupling_capacitors('10uF', '0.1uF', '0.1uF')) 162 | Net('VDD_1V8') + Ref('VDD_C')['VDD'] 163 | Net('GND') + Ref('VDD_C')['VSS'] 164 | 165 | # AON VDD decoupling caps 166 | Sub('AON_C', decoupling_capacitors('0.1uF')) 167 | Net('VDD_1V8') + Ref('AON_C')['VDD'] 168 | Net('GND') + Ref('AON_C')['VSS'] 169 | 170 | # IVDD decoupling caps 171 | Sub('IVDD_C', decoupling_capacitors('0.1uF', '0.1uF', '0.1uF')) 172 | Net('VDD_3V3') + Ref('IVDD_C')['VDD'] 173 | Net('GND') + Ref('IVDD_C')['VSS'] 174 | 175 | # PLL_AVDD decoupling caps 176 | Sub('PLL_C', decoupling_capacitors('0.1uF', '0.1uF')) 177 | Net('PLL_AVDD') + Ref('U')['PLL_AVDD'] + Ref('PLL_C')['VDD'] 178 | Net('PLL_AVSS') + Ref('U')['PLL_AVSS'] + Ref('PLL_C')['VSS'] 179 | 180 | # PLL_AVDD 181 | Node('PLL_R', 'R', '100') 182 | Net('VDD_1V8') + Ref('PLL_R')['~'] 183 | Net('PLL_AVDD') + Ref('PLL_R')['~'] 184 | 185 | # Crystal oscillator 186 | Sub('XTAL', pierce_oscillator('16MHz', '12pF')) 187 | Nets('XTAL_XI', 'XTAL_XO') + Ref('U')['XTAL_XI', 'XTAL_XO'] + \ 188 | Ref('XTAL')['XTAL_XI', 'XTAL_XO'] 189 | Net('GND') + Ref('XTAL')['GND'] 190 | 191 | if qspi_flash: 192 | Node('FLASH', 'QSPI:S') 193 | Sub('FLASH_C', decoupling_capacitors('0.1uF')) 194 | Node('SS_PULLUP', 'R', '100K') 195 | 196 | for net in ['DQ0', 'DQ1', 'DQ2', 'DQ3', 'SCLK']: 197 | Net(net) + Ref('U')['QSPI:M'][net] + Ref('FLASH')['QSPI:S'][net] 198 | Net('SS') + Ref('U')['QSPI:M']['SS0'] + Ref('FLASH')['QSPI:S']['SS'] 199 | 200 | Net('VDD_3V3') + Ref('SS_PULLUP')['~'] 201 | Ref('SS_PULLUP')['~'] + Ref('FLASH')['SS'] 202 | Net('VDD_3V3') + Refs('FLASH', 'FLASH_C')['VDD'] 203 | Net('GND') + Ref('FLASH')['GND'] + Ref('FLASH_C')['VSS'] 204 | 205 | if lfaltclk: 206 | Node('LFALTCLK', 'CLK') 207 | Sub('LFALTCLK_C', decoupling_capacitors('0.1uF')) 208 | Net() + Ref('U')['AON_PSD_LFALTCLK'] + Ref('LFALTCLK')['CLK'] 209 | Net('VDD_1V8') + Refs('LFALTCLK', 'LFALTCLK_C')['VDD'] 210 | Net('GND') + Ref('LFALTCLK')['GND'] + Ref('LFALTCLK_C')['VSS'] 211 | 212 | # Reset and Wake BTN 213 | Sub('RESET_BLK', aon_btn('RESET', debounce=True)) 214 | Net('AON_ERST_N') + Ref('U')['AON_ERST_N'] + Ref('RESET_BLK')['IN'] 215 | 216 | Sub('WAKE_BLK', aon_btn('WAKE')) 217 | Net('AON_PMU_DWAKEUP_N') + \ 218 | Ref('U')['AON_PMU_DWAKEUP_N'] + Ref('WAKE_BLK')['IN'] 219 | 220 | Net('VDD_3V3') + Refs('RESET_BLK', 'WAKE_BLK')['VDD_3V3'] 221 | Net('VDD_1V8') + Refs('RESET_BLK', 'WAKE_BLK')['VDD_1V8'] 222 | Net('GND') + Refs('RESET_BLK', 'WAKE_BLK')['GND'] 223 | 224 | # PMU 225 | Net('PMU_OUT_0') + Ref('U')['AON_PMU_OUT_0'] 226 | Net('PMU_OUT_1') + Ref('U')['AON_PMU_OUT_1'] 227 | -------------------------------------------------------------------------------- /pycircuit/formats/svg.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as xml 2 | from pycircuit.formats import extends 3 | from pycircuit.layers import RoutingLayer, PlacementLayer 4 | from pycircuit.package import Package 5 | from pycircuit.outline import * 6 | from pycircuit.pcb import Pcb 7 | 8 | 9 | class SvgElement(object): 10 | tag = '' 11 | 12 | def __init__(self, attrs={}): 13 | self.element = xml.Element(self.tag) 14 | self.set_attrs(attrs) 15 | self.children = [] 16 | 17 | def set_attrs(self, attrs): 18 | for k, v in attrs.items(): 19 | if not isinstance(v, str): 20 | v = str(v) 21 | self.set_attr(k, v) 22 | return self 23 | 24 | def set_attr(self, key, value): 25 | self.element.attrib[key] = value 26 | 27 | def get_attr(self, key): 28 | return self.element.attrib[key] 29 | 30 | def add_class(self, klass): 31 | klasses = self.get_attr('class') 32 | klasses += " " + klass 33 | self.set_attr('class', klasses) 34 | 35 | def append(self, *children): 36 | for child in children: 37 | self.element.append(child.element) 38 | self.children.append(child) 39 | return self 40 | 41 | def transform(self, transform): 42 | if 'transform' in self.element.attrib: 43 | transform = self.element.attrib['transform'] + transform 44 | 45 | self.element.attrib['transform'] = transform 46 | return self 47 | 48 | def translate(self, x, y): 49 | return self.transform('translate(%f %f)' % (x, y)) 50 | 51 | def rotate(self, angle): 52 | return self.transform('rotate(%f)' % angle) 53 | 54 | def scale(self, x, y): 55 | return self.transform('scale(%f %f)' % (x, y)) 56 | 57 | def find(self, tag): 58 | def find_rec(node, tag): 59 | for child in node: 60 | if child.tag == tag: 61 | yield child 62 | for found in find_rec(child, tag): 63 | yield found 64 | return find_rec(self.element, tag) 65 | 66 | 67 | class SvgRoot(SvgElement): 68 | tag = 'svg' 69 | 70 | def __init__(self, viewbox, attrs={}, style=''): 71 | attrs['xmlns'] = 'http://www.w3.org/2000/svg' 72 | attrs['width'] = '100%' 73 | attrs['height'] = '100%' 74 | super().__init__(attrs) 75 | self.viewbox(*viewbox) 76 | 77 | style_node = xml.Element('style') 78 | style_node.text = style 79 | self.element.append(style_node) 80 | 81 | def viewbox(self, left, top, right, bottom): 82 | return self.set_attrs({ 83 | 'viewBox': '%f %f %f %f' % (left, top, right - left, bottom - top) 84 | }) 85 | 86 | def save(self, filename): 87 | tree = xml.ElementTree(self.element) 88 | tree.write(filename) 89 | 90 | 91 | class SvgGroup(SvgElement): 92 | tag = 'g' 93 | 94 | def __init__(self, klass, *children): 95 | super().__init__({'class': klass}) 96 | self.append(*children) 97 | 98 | 99 | class SvgLine(SvgElement): 100 | tag = 'line' 101 | 102 | def __init__(self, start, end, attrs={}): 103 | attrs['x1'] = start[0] 104 | attrs['y1'] = start[1] 105 | attrs['x2'] = end[0] 106 | attrs['y2'] = end[1] 107 | super().__init__(attrs) 108 | 109 | 110 | class SvgCircle(SvgElement): 111 | tag = 'circle' 112 | 113 | def __init__(self, radius, attrs={}): 114 | if not 'cx' in attrs: 115 | attrs['cx'] = 0 116 | if not 'cy' in attrs: 117 | attrs['cy'] = 0 118 | attrs['r'] = radius 119 | super().__init__(attrs) 120 | 121 | 122 | class SvgRect(SvgElement): 123 | tag = 'rect' 124 | 125 | def __init__(self, size, attrs={}): 126 | attrs['x'] = -size[0] / 2 127 | attrs['y'] = -size[1] / 2 128 | attrs['width'] = size[0] 129 | attrs['height'] = size[1] 130 | super().__init__(attrs) 131 | 132 | 133 | class SvgText(SvgElement): 134 | tag = 'text' 135 | 136 | def __init__(self, text, attrs={}): 137 | if not 'x' in attrs: 138 | attrs['x'] = 0 139 | if not 'y' in attrs: 140 | attrs['y'] = 0 141 | if not 'text-anchor' in attrs: 142 | attrs['text-anchor'] = 'middle' 143 | super().__init__(attrs) 144 | self.element.text = text 145 | 146 | def set_text(self, text): 147 | self.element.text = text 148 | return self 149 | 150 | 151 | class SvgPath(SvgElement): 152 | tag = 'path' 153 | 154 | def set_coords(self, coords): 155 | coords = ['{0},{1}'.format(*c) for c in coords] 156 | 157 | path = 'M {0} L {1} z'.format(coords[0], ' L '.join(coords[1:])) 158 | return self.set_attrs({'d': path}) 159 | 160 | 161 | @extends(Package) 162 | def to_svg(self): 163 | # Courtyard 164 | svg_crtyd = SvgGroup('crtyd', SvgPath().set_coords(self.courtyard.coords)) 165 | 166 | # Pads 167 | svg_pads = SvgGroup('pads') 168 | svg_pad_labels = SvgGroup('pad-labels') 169 | 170 | for pad in self.pads: 171 | if pad.shape == 'rect': 172 | svg_pad = SvgRect(pad.size) 173 | elif pad.shape == 'circle': 174 | svg_pad = SvgCircle(pad.size[0] / 2) 175 | else: 176 | raise Exception('Unknown pad shape') 177 | 178 | svg_pad.set_attrs({'class': 'pad'}) 179 | svg_pad.translate(*pad.location[0:2]).rotate(-pad.angle) 180 | svg_pads.append(svg_pad) 181 | 182 | svg_label = SvgText(pad.name, {'dy': '.4em'}) 183 | svg_label.set_attrs({'class': 'pad-label'}) 184 | svg_label.translate(*pad.location[0:2]) 185 | svg_pad_labels.append(svg_label) 186 | 187 | # Reference 188 | text_radius = self.size()[1] / 2 189 | svg_ref = SvgText(self.name, {'class': 'ref', 'y': -text_radius - 0.2}) 190 | 191 | return SvgRoot(self.courtyard.bounds) \ 192 | .append(svg_crtyd, svg_pads, svg_pad_labels, svg_ref) 193 | 194 | 195 | @extends(PlacementLayer) 196 | def to_svg(self): 197 | def transform(elem, x, y, angle, no_flip=False): 198 | elem.translate(x, y).rotate(-angle) 199 | if not no_flip and self.flip: 200 | elem.scale(1, -1) 201 | return elem 202 | 203 | pads_layer = SvgGroup('pads_layer') 204 | pad_labels_layer = SvgGroup('pad_labels_layer') 205 | crtyd_layer = SvgGroup('crtyd_layer') 206 | ref_layer = SvgGroup('ref_layer') 207 | 208 | for inst in self.insts: 209 | package = inst.device.package.to_svg() 210 | crtyd, pads, pad_labels, ref = package.children 211 | ref.set_text(inst.name) 212 | 213 | x, y, angle = inst.attributes.x, inst.attributes.y, inst.attributes.angle 214 | 215 | pads_layer.append(transform(pads, x, y, angle)) 216 | crtyd_layer.append(transform(crtyd, x, y, angle)) 217 | ref_layer.append(transform(ref, x, y, angle, no_flip=True)) 218 | pad_labels_layer.append( 219 | transform(pad_labels, x, y, angle, no_flip=True)) 220 | 221 | layer = SvgGroup('layer') 222 | layer.add_class(self.layer.name) 223 | layer.append(pads_layer) 224 | layer.append(pad_labels_layer) 225 | layer.append(crtyd_layer) 226 | layer.append(ref_layer) 227 | 228 | return layer 229 | 230 | 231 | @extends(RoutingLayer) 232 | def to_svg(self): 233 | layer = SvgGroup('layer') 234 | layer.add_class(self.layer.name) 235 | 236 | for via in self.vias: 237 | svia = SvgGroup('via', 238 | SvgCircle(via.diameter / 2, { 239 | 'class': 'dia' 240 | }), 241 | SvgCircle(via.drill / 2, { 242 | 'class': 'drill' 243 | }) 244 | ).translate(via.coord[0], via.coord[1]) 245 | layer.append(svia) 246 | 247 | for seg in self.segments: 248 | sseg = SvgLine(seg.start, seg.end, { 249 | 'class': 'seg', 250 | 'stroke-width': seg.width 251 | }) 252 | layer.append(sseg) 253 | 254 | return layer 255 | 256 | 257 | @extends(Outline) 258 | def to_svg(self): 259 | layer = SvgGroup('outline') 260 | layer.append(SvgPath().set_coords(self.polygon.exterior.coords)) 261 | for f in self.features: 262 | if isinstance(f, Hole): 263 | f = SvgCircle(f.drill_size / 2, { 264 | 'cx': f.position[0], 265 | 'cy': f.position[1] 266 | }) 267 | layer.append(f) 268 | return layer 269 | 270 | 271 | @extends(Pcb) 272 | def to_svg(self, path): 273 | bounds = self.outline.polygon.exterior.bounds 274 | with open('../../viewer/css/pcb.svg.css') as f: 275 | style = f.read() 276 | svg = SvgRoot(bounds, style=style) 277 | 278 | top = self.attributes.layers.placement_layers[0] 279 | bottom = self.attributes.layers.placement_layers[-1] 280 | 281 | svg.append(bottom.to_svg()) 282 | 283 | for layer in reversed(self.attributes.layers.routing_layers): 284 | svg.append(layer.to_svg()) 285 | 286 | svg.append(top.to_svg()) 287 | 288 | svg.append(self.outline.to_svg()) 289 | 290 | svg.save(path) 291 | -------------------------------------------------------------------------------- /pycircuit/library/old/lattice/lattice.py: -------------------------------------------------------------------------------- 1 | from pycircuit.device import * 2 | from pycircuit.footprint import * 3 | 4 | 5 | # iCE40 FPGA Family 6 | Device('HX1K', 7 | # IO Left 8 | Pin('IOL_1A', Bus('DPIO1', 'DPIO', '-')), 9 | Pin('IOL_1B', Bus('DPIO1', 'DPIO', '+')), 10 | Pin('IOL_2A', Bus('DPIO2', 'DPIO', '-')), 11 | Pin('IOL_2B', Bus('DPIO2', 'DPIO', '+')), 12 | Pin('IOL_3A', Bus('DPIO3', 'DPIO', '-')), 13 | Pin('IOL_3B', Bus('DPIO3', 'DPIO', '+')), 14 | Pin('IOL_4A', Bus('DPIO4', 'DPIO', '-')), 15 | Pin('IOL_4B', Bus('DPIO4', 'DPIO', '+')), 16 | Pin('IOL_5A', Bus('DPIO5', 'DPIO', '-')), 17 | Pin('IOL_5B', Bus('DPIO5', 'DPIO', '+')), 18 | Pin('IOL_6A', Bus('DPIO6', 'DPIO', '-')), 19 | Pin('IOL_6B_GBIN7', Bus('DPIO6', 'DPIO', '+'), Bus('GBIN', '7')), 20 | Pin('IOL_7A_GBIN6', Bus('DPIO7', 'DPIO', '-'), Bus('GBIN', '6')), 21 | Pin('IOL_7B', Bus('DPIO7', 'DPIO', '+')), 22 | Pin('IOL_8A', Bus('DPIO8', 'DPIO', '-')), 23 | Pin('IOL_8B', Bus('DPIO8', 'DPIO', '+')), 24 | Pin('IOL_9A', Bus('DPIO9', 'DPIO', '-')), 25 | Pin('IOL_9B', Bus('DPIO9', 'DPIO', '+')), 26 | Pin('IOL_10A', Bus('DPIO10', 'DPIO', '-')), 27 | Pin('IOL_10B', Bus('DPIO10', 'DPIO', '+')), 28 | Pin('IOL_11A', Bus('DPIO11', 'DPIO', '-')), 29 | Pin('IOL_11B', Bus('DPIO11', 'DPIO', '+')), 30 | Pin('IOL_12A', Bus('DPIO12', 'DPIO', '-')), 31 | Pin('IOL_12B', Bus('DPIO12', 'DPIO', '+')), 32 | # PLL_VCC 33 | Pin('GNDPLL'), 34 | Pin('VCCPLL'), # 1V2 35 | 36 | # IO Bottom 37 | Pin('IOB_24', 'PIO'), 38 | Pin('IOB_25', 'PIO'), 39 | Pin('IOB_26', 'PIO'), 40 | Pin('IOB_27', 'PIO'), 41 | Pin('IOB_28', 'PIO'), 42 | Pin('IOB_29', 'PIO'), 43 | Pin('IOB_30', 'PIO'), 44 | Pin('IOB_31', 'PIO'), 45 | Pin('IOB_32', 'PIO'), 46 | Pin('IOB_33', 'PIO'), 47 | Pin('IOB_35_GBIN5', 'PIO', Bus('GBIN', '5')), 48 | Pin('IOB_36_GBIN4', 'PIO', Bus('GBIN', '4')), 49 | Pin('IOB_34', 'PIO'), 50 | Pin('IOB_37', 'PIO'), 51 | Pin('IOB_38', 'PIO'), 52 | Pin('IOB_39', 'PIO'), 53 | Pin('IOB_40', 'PIO'), 54 | Pin('IOB_41', 'PIO'), 55 | Pin('IOB_42_CBSEL0', 'PIO', Bus('CBSEL', '0')), 56 | Pin('IOB_43_CBSEL1', 'PIO', Bus('CBSEL', '1')), 57 | # CONFIG 58 | Pin('CDONE'), 59 | Pin('CRESET_B'), 60 | # SPI 61 | Pin('IOB_44_SDO', 'PIO', Bus('SPI', 'MISO')), 62 | Pin('IOB_45_SDI', 'PIO', Bus('SPI', 'MOSI')), 63 | Pin('IOB_46_SCK', 'PIO', Bus('SPI', 'SCK')), 64 | Pin('IOB_47_SS', 'PIO', Bus('SPI', 'SS0')), 65 | Pin('VCC_SPI'), # 3V3 66 | 67 | # IO Right 68 | Pin('IOR_48', 'PIO'), 69 | Pin('IOR_49', 'PIO'), 70 | Pin('IOR_50', 'PIO'), 71 | Pin('IOR_51', 'PIO'), 72 | Pin('IOR_52', 'PIO'), 73 | Pin('IOR_53', 'PIO'), 74 | Pin('IOR_54', 'PIO'), 75 | Pin('IOR_55', 'PIO'), 76 | Pin('IOR_56', 'PIO'), 77 | Pin('IOR_57', 'PIO'), 78 | Pin('IOR_58', 'PIO'), 79 | Pin('IOR_59', 'PIO'), 80 | Pin('IOR_60_GBIN3', 'PIO', Bus('GBIN', '3')), 81 | Pin('IOR_61_GBIN2', 'PIO', Bus('GBIN', '2')), 82 | Pin('IOR_62', 'PIO'), 83 | Pin('IOR_63', 'PIO'), 84 | Pin('IOR_64', 'PIO'), 85 | Pin('IOR_65', 'PIO'), 86 | Pin('IOR_66', 'PIO'), 87 | Pin('IOR_67', 'PIO'), 88 | Pin('IOR_68', 'PIO'), 89 | Pin('IOR_69', 'PIO'), 90 | Pin('IOR_70', 'PIO'), 91 | Pin('IOR_71', 'PIO'), 92 | Pin('IOR_72', 'PIO'), 93 | Pin('VPP_2V5'), # 2V5 94 | Pin('VPP_FAST'), 95 | 96 | # IO Top 97 | Pin('IOT_73', 'PIO'), 98 | Pin('IOT_74', 'PIO'), 99 | Pin('IOT_75', 'PIO'), 100 | Pin('IOT_76', 'PIO'), 101 | Pin('IOT_77', 'PIO'), 102 | Pin('IOT_78', 'PIO'), 103 | Pin('IOT_79', 'PIO'), 104 | Pin('IOT_80', 'PIO'), 105 | Pin('IOT_81', 'PIO'), 106 | Pin('IOT_82', 'PIO'), 107 | Pin('IOT_83', 'PIO'), 108 | Pin('IOT_84_GBIN1', 'PIO', Bus('GBIN', '1')), 109 | Pin('IOT_85_GBIN0', 'PIO', Bus('GBIN', '0')), 110 | Pin('IOT_86', 'PIO'), 111 | Pin('IOT_87', 'PIO'), 112 | Pin('IOT_88', 'PIO'), 113 | Pin('IOT_89', 'PIO'), 114 | Pin('IOT_90', 'PIO'), 115 | Pin('IOT_91', 'PIO'), 116 | Pin('IOT_92', 'PIO'), 117 | Pin('IOT_93', 'PIO'), 118 | Pin('IOT_94', 'PIO'), 119 | Pin('IOT_95', 'PIO'), 120 | Pin('IOT_96', 'PIO'), 121 | Pin('GND'), 122 | Pin('VCC'), # 1V2 123 | Pin('VCCIO_0'), # 3V3 124 | Pin('VCCIO_1'), # 3V3 125 | Pin('VCCIO_2'), # 3V3 126 | Pin('VCCIO_3')) # 3V3 127 | 128 | 129 | Footprint('HX1K-TQFP144', 'HX1K', 'TQFP144', 130 | Map(1, 'IOL_1A'), 131 | Map(2, 'IOL_1B'), 132 | Map(3, 'IOL_2A'), 133 | Map(4, 'IOL_2B'), 134 | Map(5, 'GND'), 135 | Map(6, 'VCCIO_3'), 136 | Map(7, 'IOL_3A'), 137 | Map(8, 'IOL_3B'), 138 | Map(9, 'IOL_4A'), 139 | Map(10, 'IOL_4B'), 140 | Map(11, 'IOL_5A'), 141 | Map(12, 'IOL_5B'), 142 | Map(13, 'GND'), 143 | Map(14, 'GND'), 144 | Map(15, None), 145 | Map(16, None), 146 | Map(17, None), 147 | Map(18, None), 148 | Map(19, 'IOL_6A'), 149 | Map(20, 'IOL_6B_GBIN7'), 150 | Map(21, 'IOL_7A_GBIN6'), 151 | Map(22, 'IOL_7B'), 152 | Map(23, 'IOL_8A'), 153 | Map(24, 'IOL_8B'), 154 | Map(25, 'IOL_9A'), 155 | Map(26, 'IOL_9B'), 156 | Map(27, 'VCC'), 157 | Map(28, 'IOL_10A'), 158 | Map(29, 'IOL_10B'), 159 | Map(30, 'VCCIO_3'), 160 | Map(31, 'IOL_11A'), 161 | Map(32, 'IOL_11B'), 162 | Map(33, 'IOL_12A'), 163 | Map(34, 'IOL_12B'), 164 | Map(35, 'GNDPLL'), 165 | Map(36, 'VCCPLL'), 166 | Map(37, 'IOB_24'), 167 | Map(38, 'IOB_25'), 168 | Map(39, 'IOB_26'), 169 | Map(40, None), 170 | Map(41, 'IOB_27'), 171 | Map(42, 'IOB_28'), 172 | Map(43, 'IOB_29'), 173 | Map(44, 'IOB_30'), 174 | Map(45, 'IOB_31'), 175 | Map(46, 'VCCIO_3'), 176 | Map(47, 'IOB_32'), 177 | Map(48, 'IOB_33'), 178 | Map(49, 'IOB_35_GBIN5'), 179 | Map(50, 'IOB_36_GBIN4'), 180 | Map(51, 'VCC'), 181 | Map(52, 'IOB_34'), 182 | Map(53, None), 183 | Map(54, None), 184 | Map(55, None), 185 | Map(56, 'IOB_37'), 186 | Map(57, 'VCCIO_2'), 187 | Map(58, 'IOB_38'), 188 | Map(59, 'GND'), 189 | Map(60, 'IOB_39'), 190 | Map(61, 'IOB_40'), 191 | Map(62, 'IOB_41'), 192 | Map(63, 'IOB_42_CBSEL0'), 193 | Map(64, 'IOB_43_CBSEL1'), 194 | Map(65, 'CDONE'), 195 | Map(66, 'CRESET_B'), 196 | Map(67, 'IOB_44_SDO'), 197 | Map(68, 'IOB_45_SDI'), 198 | Map(69, 'GND'), 199 | Map(70, 'IOB_46_SCK'), 200 | Map(71, 'IOB_47_SS'), 201 | Map(72, 'VCC_SPI'), 202 | Map(73, 'IOR_48'), 203 | Map(74, 'IOR_49'), 204 | Map(75, 'IOR_50'), 205 | Map(76, 'IOR_51'), 206 | Map(77, None), 207 | Map(78, 'IOR_52'), 208 | Map(79, 'IOR_53'), 209 | Map(80, 'IOR_54'), 210 | Map(81, 'IOR_55'), 211 | Map(82, None), 212 | Map(83, None), 213 | Map(84, None), 214 | Map(85, None), 215 | Map(86, 'GND'), 216 | Map(87, 'IOR_56'), 217 | Map(88, 'IOR_57'), 218 | Map(89, 'VCCIO_1'), 219 | Map(90, 'IOR_58'), 220 | Map(91, 'IOR_59'), 221 | Map(92, 'VCC'), 222 | Map(93, 'IOR_60_GBIN3'), 223 | Map(94, 'IOR_61_GBIN2'), 224 | Map(95, 'IOR_62'), 225 | Map(96, 'IOR_63'), 226 | Map(97, 'IOR_64'), 227 | Map(98, 'IOR_65'), 228 | Map(99, 'IOR_66'), 229 | Map(100, 'VCCIO_1'), 230 | Map(101, 'IOR_67'), 231 | Map(102, 'IOR_68'), 232 | Map(103, 'GND'), 233 | Map(104, 'IOR_69'), 234 | Map(105, 'IOR_70'), 235 | Map(106, 'IOR_71'), 236 | Map(107, 'IOR_72'), 237 | Map(108, 'VPP_2V5'), 238 | Map(109, 'VPP_FAST'), 239 | Map(110, None), 240 | Map(111, 'VCC'), 241 | Map(112, 'IOT_73'), 242 | Map(113, 'IOT_74'), 243 | Map(114, 'IOT_75'), 244 | Map(115, 'IOT_76'), 245 | Map(116, 'IOT_77'), 246 | Map(117, 'IOT_78'), 247 | Map(118, 'IOT_79'), 248 | Map(119, 'IOT_80'), 249 | Map(120, 'IOT_81'), 250 | Map(121, 'IOT_82'), 251 | Map(122, 'IOT_83'), 252 | Map(123, 'VCCIO_0'), 253 | Map(124, None), 254 | Map(125, None), 255 | Map(126, None), 256 | Map(127, None), 257 | Map(128, 'IOT_84_GBIN1'), 258 | Map(129, 'IOT_85_GBIN0'), 259 | Map(None, 'IOT_86'), 260 | Map(130, None), 261 | Map(131, None), 262 | Map(132, 'GND'), 263 | Map(133, 'VCCIO_0'), 264 | Map(134, 'IOT_87'), 265 | Map(135, 'IOT_88'), 266 | Map(136, 'IOT_89'), 267 | Map(137, 'IOT_90'), 268 | Map(138, 'IOT_91'), 269 | Map(139, 'IOT_92'), 270 | Map(140, 'GND'), 271 | Map(141, 'IOT_93'), 272 | Map(142, 'IOT_94'), 273 | Map(143, 'IOT_95'), 274 | Map(144, 'IOT_96')) 275 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "08aa94e0e4afd72a99e6bf94d89960f1018b9a274b3db03c1fd391fca1a6f173" 5 | }, 6 | "host-environment-markers": { 7 | "implementation_name": "cpython", 8 | "implementation_version": "3.6.3", 9 | "os_name": "posix", 10 | "platform_machine": "x86_64", 11 | "platform_python_implementation": "CPython", 12 | "platform_release": "4.12.14", 13 | "platform_system": "Linux", 14 | "platform_version": "#1 SMP 1", 15 | "python_full_version": "3.6.3", 16 | "python_version": "3.6", 17 | "sys_platform": "linux" 18 | }, 19 | "pipfile-spec": 6, 20 | "requires": { 21 | "python_version": "3.6" 22 | }, 23 | "sources": [ 24 | { 25 | "name": "pypi", 26 | "url": "https://pypi.python.org/simple", 27 | "verify_ssl": true 28 | } 29 | ] 30 | }, 31 | "default": { 32 | "numpy": { 33 | "hashes": [ 34 | "sha256:428cd3c0b197cf857671353d8c85833193921af9fafcc169a1f29c7185833d50", 35 | "sha256:a476e437d73e5754aa66e1e75840d0163119c3911b7361f4cd06985212a3c3fb", 36 | "sha256:289ff717138cd9aa133adcbd3c3e284458b9c8230db4d42b39083a3407370317", 37 | "sha256:c5eccb4bf96dbb2436c61bb3c2658139e779679b6ae0d04c5e268e6608b58053", 38 | "sha256:75471acf298d455b035226cc609a92aee42c4bb6aa71def85f77fa2c2b646b61", 39 | "sha256:5c54fb98ecf42da59ed93736d1c071842482b18657eb16ba6e466bd873e1b923", 40 | "sha256:9ddf384ac3aacb72e122a8207775cc29727cbd9c531ee1a4b95754f24f42f7f3", 41 | "sha256:781d3197da49c421a07f250750de70a52c42af08ca02a2f7bdb571c0625ae7eb", 42 | "sha256:93b26d6c06a22e64d56aaca32aaaffd27a4143db0ac2f21a048f0b571f2bfc55", 43 | "sha256:b2547f57d05ba59df4289493254f29f4c9082d255f1f97b7e286f40f453e33a1", 44 | "sha256:eef6af1c752eef538a96018ef9bdf8e37bbf28aab50a1436501a4aa47a6467df", 45 | "sha256:ff8a4b2c3ac831964f529a2da506c28d002562b230261ae5c16885f5f53d2e75", 46 | "sha256:194074058c22a4066e1b6a4ea432486ee468d24ab16f13630c1030409e6b8666", 47 | "sha256:4e13f1a848fde960dea33702770265837c72b796a6a3eaac7528cfe75ddefadd", 48 | "sha256:91101216d72749df63968d86611b549438fb18af2c63849c01f9a897516133c7", 49 | "sha256:97507349abb7d1f6b76b877258defe8720833881dc7e7fd052bac90c88587387", 50 | "sha256:1479b46b6040b5c689831496354c8859c456b152d37315673a0c18720b41223b", 51 | "sha256:98b1ac79c160e36093d7914244e40ee1e7164223e795aa2c71dcce367554e646", 52 | "sha256:24bbec9a199f938eab75de8390f410969bc33c218e5430fa1ae9401b00865255", 53 | "sha256:7880f412543e96548374a4bb1d75e4cdb8cad80f3a101ed0f8d0e0428f719c1c", 54 | "sha256:6112f152b76a28c450bbf665da11757078a724a90330112f5b7ea2d6b6cefd67", 55 | "sha256:7c5276763646480143d5f3a6c2acb2885460c765051a1baf4d5070f63d05010f", 56 | "sha256:3de643935b212307b420248018323a44ec51987a336d1d747c1322afc3c099fb" 57 | ], 58 | "version": "==1.14.0" 59 | }, 60 | "pykicad": { 61 | "hashes": [ 62 | "sha256:ded058f3af116845d8a0495f3a8ff4b6a7e707e534c5b71ee61b37fb23553923" 63 | ], 64 | "version": "==0.1.1" 65 | }, 66 | "pyparsing": { 67 | "hashes": [ 68 | "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010", 69 | "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", 70 | "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", 71 | "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", 72 | "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", 73 | "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", 74 | "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58" 75 | ], 76 | "version": "==2.2.0" 77 | }, 78 | "shapely": { 79 | "hashes": [ 80 | "sha256:13c82c9110e630039163bdcdb9969db4fadd145bcf633fc678fa762b01686e6d", 81 | "sha256:cf02668c8633a41782b5244e04d5d0689847ac535cdf2fea66e4cac7dd772895", 82 | "sha256:049e7732c58f8ee143899f9efbf714d8e7e3578443883c95614cfd4891d36502", 83 | "sha256:c1f4d74e4c4a5681e77fbee5a7b0599a91ca39a8e260f2839a9a764382994f7f", 84 | "sha256:41850cbfc79de117e3ccbc025b2eb7ee9170c29e65910003828e560ff076fc0d", 85 | "sha256:a0db70e3384f1227bc40e22e9d77db3a54e02b54b48cf6b0c22ddfc0b688542d", 86 | "sha256:592a7821c00ef4e0ae07e952366ad537313ccb7488665dd54b87e6f8eb31ef80", 87 | "sha256:2222eba7f461f4b3f406b569727561f300ecd57e31e01b49ffd327dc4cbda632", 88 | "sha256:e848bf533f0d6cbc336b4cf01dd1aa7873174ba7304a620baa7d663d1d0ce879", 89 | "sha256:30df7572d311514802df8dc0e229d1660bc4cbdcf027a8281e79c5fc2fcf02f2" 90 | ], 91 | "version": "==1.6.4.post1" 92 | } 93 | }, 94 | "develop": { 95 | "attrs": { 96 | "hashes": [ 97 | "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", 98 | "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" 99 | ], 100 | "version": "==17.4.0" 101 | }, 102 | "autopep8": { 103 | "hashes": [ 104 | "sha256:c7be71ab0cb2f50c9c22c82f0c9acaafc6f57492c3fbfee9790c415005c2b9a5" 105 | ], 106 | "version": "==1.3.4" 107 | }, 108 | "certifi": { 109 | "hashes": [ 110 | "sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296", 111 | "sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d" 112 | ], 113 | "version": "==2018.1.18" 114 | }, 115 | "chardet": { 116 | "hashes": [ 117 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", 118 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae" 119 | ], 120 | "version": "==3.0.4" 121 | }, 122 | "idna": { 123 | "hashes": [ 124 | "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4", 125 | "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f" 126 | ], 127 | "version": "==2.6" 128 | }, 129 | "pkginfo": { 130 | "hashes": [ 131 | "sha256:31a49103180ae1518b65d3f4ce09c784e2bc54e338197668b4fb7dc539521024", 132 | "sha256:bb1a6aeabfc898f5df124e7e00303a5b3ec9a489535f346bfbddb081af93f89e" 133 | ], 134 | "version": "==1.4.1" 135 | }, 136 | "pluggy": { 137 | "hashes": [ 138 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" 139 | ], 140 | "version": "==0.6.0" 141 | }, 142 | "py": { 143 | "hashes": [ 144 | "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", 145 | "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" 146 | ], 147 | "version": "==1.5.2" 148 | }, 149 | "pycodestyle": { 150 | "hashes": [ 151 | "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9", 152 | "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766" 153 | ], 154 | "version": "==2.3.1" 155 | }, 156 | "pytest": { 157 | "hashes": [ 158 | "sha256:95fa025cd6deb5d937e04e368a00552332b58cae23f63b76c8c540ff1733ab6d", 159 | "sha256:6074ea3b9c999bd6d0df5fa9d12dd95ccd23550df2a582f5f5b848331d2e82ca" 160 | ], 161 | "version": "==3.4.0" 162 | }, 163 | "requests": { 164 | "hashes": [ 165 | "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", 166 | "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" 167 | ], 168 | "version": "==2.18.4" 169 | }, 170 | "requests-toolbelt": { 171 | "hashes": [ 172 | "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", 173 | "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" 174 | ], 175 | "version": "==0.8.0" 176 | }, 177 | "six": { 178 | "hashes": [ 179 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", 180 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" 181 | ], 182 | "version": "==1.11.0" 183 | }, 184 | "tqdm": { 185 | "hashes": [ 186 | "sha256:4c041f8019f7be65b8028ddde9a836f7ccc51c4637f1ff2ba9b5813d38d19d5a", 187 | "sha256:df32e6f127dc0ccbc675eadb33f749abbcb8f174c5cb9ec49c0cdb73aa737377" 188 | ], 189 | "version": "==4.19.5" 190 | }, 191 | "twine": { 192 | "hashes": [ 193 | "sha256:d3ce5c480c22ccfb761cd358526e862b32546d2fe4bc93d46b5cf04ea3cc46ca", 194 | "sha256:caa45b7987fc96321258cd7668e3be2ff34064f5c66d2d975b641adca659c1ab" 195 | ], 196 | "version": "==1.9.1" 197 | }, 198 | "urllib3": { 199 | "hashes": [ 200 | "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", 201 | "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" 202 | ], 203 | "version": "==1.22" 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /pycircuit/pcb.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import shapely.ops as ops 3 | from shapely.geometry import Point, Polygon 4 | from pycircuit.circuit import Netlist 5 | from pycircuit.device import Device 6 | from pycircuit.outline import Outline, OutlineDesignRules 7 | from pycircuit.layers import Layers 8 | from pycircuit.traces import NetClass, TraceDesignRules, Segment, Via 9 | 10 | 11 | class Matrix(object): 12 | '''A collection of functions for generating transformation 13 | matrices with numpy and applying them to shapely geometry.''' 14 | 15 | @staticmethod 16 | def translation(x, y): 17 | '''Returns a translation matrix.''' 18 | 19 | return np.array([[1, 0, x], 20 | [0, 1, y], 21 | [0, 0, 1]]) 22 | 23 | @staticmethod 24 | def rotation(angle): 25 | '''Returns a rotation matrix for a coordinate system with an inverted 26 | y-axis [oo, -oo]. The unit of angle is degrees.''' 27 | 28 | theta = (-angle / 180) * np.pi 29 | return np.array([[np.cos(theta), -np.sin(theta), 0], 30 | [np.sin(theta), np.cos(theta), 0], 31 | [0, 0, 1]]) 32 | 33 | @staticmethod 34 | def flip(): 35 | '''Returns a matrix that mirrors accross the y-axis.''' 36 | 37 | return np.array([[1, 0, 0], 38 | [0, -1, 0], 39 | [0, 0, 1]]) 40 | 41 | @staticmethod 42 | def transform(geometry, matrix): 43 | '''Returns a new shapely geometry transformed with matrix.''' 44 | 45 | def apply_matrix(xs, ys): 46 | res_xs, res_ys = [], [] 47 | for x, y in zip(xs, ys): 48 | vec = np.array([x, y, 1]) 49 | res = matrix.dot(np.array([x, y, 1])) 50 | res_xs.append(res[0]) 51 | res_ys.append(res[1]) 52 | return (res_xs, res_ys) 53 | 54 | return ops.transform(apply_matrix, geometry) 55 | 56 | @staticmethod 57 | def inst_matrix(x, y, angle, flip): 58 | if flip: 59 | flip = Matrix.flip() 60 | else: 61 | flip = np.identity(3) 62 | 63 | return Matrix.translation(x, y).dot(Matrix.rotation(angle)).dot(flip) 64 | 65 | 66 | class AbsolutePad(object): 67 | def __init__(self, inst, pad, matrix): 68 | self.inst = inst 69 | self.pad = pad 70 | self.location = matrix.dot(pad.location) 71 | self.size = pad.size 72 | 73 | def __repr__(self): 74 | return 'pad %s %s' % (self.inst.name, self.pad.name) 75 | 76 | 77 | class NetAttributes(object): 78 | def __init__(self, net, net_class, pcb): 79 | self.net = net 80 | self.net.attributes = self 81 | self.pcb = pcb 82 | 83 | self.net_class = net_class 84 | self.segments = [] 85 | self.vias = [] 86 | 87 | def iter_pads(self): 88 | '''Iterator over all coordinates belonging to the net.''' 89 | 90 | for assign in self.net.assigns: 91 | for pad in assign.inst.attributes.pads_by_pin(assign.pin): 92 | yield pad 93 | 94 | def bounds(self): 95 | '''Returns a tuple (min_x, min_y, max_x, max_y) of all the coordinates 96 | connected by the net. Useful for placement algorithms.''' 97 | 98 | return asMultiPoint(list(self.pad_locations())).bounds 99 | 100 | def size(self): 101 | '''Returns a tuple (width, height) of the net. Useful for placement 102 | algorithms.''' 103 | 104 | min_x, min_y, max_x, max_y = self.bounds() 105 | return max_x - min_x, max_y - min_y 106 | 107 | def half_perimeter_length(self): 108 | '''Returns an estimation of the wire length from the boundary 109 | of it's coordinates. Useful for placement algorithms.''' 110 | 111 | return sum(self.size()) 112 | 113 | def length(self): 114 | '''Returns the geometric length of the net by summing 115 | the length of it's segments.''' 116 | 117 | length = 0 118 | for segment in self.segments: 119 | length += segment.length() 120 | return length 121 | 122 | def to_object(self): 123 | return { 124 | self.net.name: { 125 | 'net_class': self.net_class.uid, 126 | 'segments': [seg.to_object() for seg in self.segments], 127 | 'vias': [via.to_object() for via in self.vias], 128 | } 129 | } 130 | 131 | @classmethod 132 | def from_object(cls, obj, net, pcb): 133 | net_class = pcb.net_class_by_uid(obj['net_class']) 134 | attrs = cls(net, net_class, pcb) 135 | for seg in obj['segments']: 136 | Segment.from_object(seg, pcb) 137 | for via in obj['vias']: 138 | Via.from_object(via, pcb) 139 | return attrs 140 | 141 | 142 | class InstAttributes(object): 143 | def __init__(self, inst, pcb): 144 | assert isinstance(inst.device, Device) 145 | 146 | self.inst = inst 147 | self.inst.attributes = self 148 | self.pcb = pcb 149 | 150 | self.layer = pcb.attributes.layers.placement_layers[0] 151 | self.x = 0 152 | self.y = 0 153 | self.angle = 0 154 | self.matrix = None 155 | 156 | def iter_pads(self): 157 | for pad in self.inst.device.package.pads: 158 | yield AbsolutePad(self.inst, pad, self.matrix) 159 | 160 | def pad_by_name(self, name): 161 | pad = self.inst.device.package.pad_by_name(name) 162 | return AbsolutePad(self.inst, pad, self.matrix) 163 | 164 | def pads_by_pin(self, pin): 165 | for pad in self.inst.device.pads_by_pin(pin): 166 | yield AbsolutePad(self.inst, pad, self.matrix) 167 | 168 | def courtyard(self): 169 | crtyd = self.inst.device.package.courtyard.polygon 170 | return Matrix.transform(crtyd, self.matrix) 171 | 172 | def place(self, layer, x, y, angle=0): 173 | '''Places the node.''' 174 | 175 | self.layer = layer 176 | self.angle = angle 177 | self.x = x 178 | self.y = y 179 | self.matrix = Matrix.inst_matrix(x, y, angle, layer.flip) 180 | layer.insts.append(self.inst) 181 | 182 | def to_object(self): 183 | return { 184 | self.inst.name: { 185 | 'x': self.x, 186 | 'y': self.y, 187 | 'angle': self.angle, 188 | 'flip': self.layer.flip, 189 | 'layer': self.layer.layer.name, 190 | 'package': self.inst.device.package.name, 191 | } 192 | } 193 | 194 | @classmethod 195 | def from_object(cls, obj, inst, pcb): 196 | attrs = cls(inst, pcb) 197 | player = pcb.attributes.layers.player_by_name(obj['layer']) 198 | attrs.place(player, obj['x'], obj['y'], obj['angle']) 199 | return attrs 200 | 201 | 202 | class PcbAttributes(object): 203 | def __init__(self, layers, outline_design_rules, trace_design_rules, 204 | cost_cm2): 205 | self.layers = layers 206 | self.outline_design_rules = outline_design_rules 207 | self.trace_design_rules = trace_design_rules 208 | self.cost_cm2 = cost_cm2 209 | 210 | def to_object(self): 211 | return { 212 | 'layers': self.layers.to_object(), 213 | 'outline_design_rules': self.outline_design_rules.to_object(), 214 | 'trace_design_rules': self.trace_design_rules.to_object(), 215 | 'cost_cm2': self.cost_cm2, 216 | } 217 | 218 | @classmethod 219 | def from_object(cls, obj): 220 | return cls(Layers.from_object(obj['layers']), 221 | OutlineDesignRules.from_object(obj['outline_design_rules']), 222 | TraceDesignRules.from_object(obj['trace_design_rules']), 223 | obj['cost_cm2']) 224 | 225 | 226 | class Pcb(object): 227 | def __init__(self, netlist, outline, attributes, _init=True): 228 | assert isinstance(netlist, Netlist) 229 | assert isinstance(outline, Outline) 230 | assert isinstance(attributes, PcbAttributes) 231 | 232 | self.netlist = netlist 233 | self.outline = outline 234 | self.attributes = attributes 235 | self.net_classes = [attributes.trace_design_rules.to_netclass()] 236 | 237 | if _init: 238 | for inst in self.netlist.insts: 239 | InstAttributes(inst, self) 240 | 241 | for net in self.netlist.nets: 242 | nc = NetClass(self.net_classes[0]) 243 | self.net_classes.append(nc) 244 | NetAttributes(net, nc, self) 245 | 246 | def net_class_by_uid(self, uid): 247 | for nc in self.net_classes: 248 | if nc.uid == uid: 249 | return nc 250 | 251 | def size(self): 252 | bounds = self.outline.polygon.exterior.bounds 253 | width = bounds[2] - bounds[0] 254 | height = bounds[3] - bounds[1] 255 | return width, height 256 | 257 | def area(self): 258 | return self.outline.polygon.exterior.area 259 | 260 | def cost(self): 261 | return self.outline.polygon.exterior.area / 100 * self.attributes.cost_cm2 262 | 263 | def to_object(self): 264 | packages = {} 265 | insts = {} 266 | nets = {} 267 | for inst in self.netlist.insts: 268 | pck = inst.device.package 269 | packages.update(pck.to_object()) 270 | insts.update(inst.attributes.to_object()) 271 | for net in self.netlist.nets: 272 | nets.update(net.attributes.to_object()) 273 | 274 | return { 275 | 'netlist': self.netlist.to_object(), 276 | 'outline': self.outline.to_object(), 277 | 'attributes': self.attributes.to_object(), 278 | 'packages': packages, 279 | 'insts': insts, 280 | 'nets': nets, 281 | 'net_classes': [nc.to_object() for nc in self.net_classes], 282 | } 283 | 284 | @classmethod 285 | def from_object(cls, obj): 286 | pcb = cls(Netlist.from_object(obj['netlist']), 287 | Outline.from_object(obj['outline']), 288 | PcbAttributes.from_object(obj['attributes']), 289 | _init=False) 290 | 291 | for nc in obj['net_classes']: 292 | pcb.net_classes.append(NetClass.from_object(nc, pcb)) 293 | 294 | for inst in pcb.netlist.insts: 295 | inst_obj = obj['insts'][inst.name] 296 | InstAttributes.from_object(inst_obj, inst, pcb) 297 | 298 | for net in pcb.netlist.nets: 299 | net_obj = obj['nets'][net.name] 300 | NetAttributes.from_object(net_obj, net, pcb) 301 | 302 | return pcb 303 | -------------------------------------------------------------------------------- /pycircuit/package.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from shapely import affinity 4 | from shapely.geometry import Point, Polygon 5 | 6 | 7 | class Pad(object): 8 | 9 | def __init__(self, name, x, y, angle=0, size=None, shape=None, drill=None): 10 | self.name = name 11 | self.angle = angle 12 | 13 | point = affinity.rotate(Point(x, y), angle, origin=Point(0, 0)) 14 | self.location = np.array([point.coords[0][0], point.coords[0][1], 1]) 15 | 16 | # Optional properties for pcb export 17 | self.size = size 18 | self.shape = shape 19 | self.drill = drill 20 | 21 | def __repr__(self): 22 | return '%s (%s, %s)' % (self.name, self.location[0], self.location[1]) 23 | 24 | def to_object(self): 25 | return { 26 | self.name: { 27 | 'x': self.location[0], 28 | 'y': self.location[1], 29 | 'r': self.angle, 30 | } 31 | } 32 | 33 | 34 | class PadArray(object): 35 | def __init__(self, length, pitch=0, radius=0, angle=0, prefix='', offset=0): 36 | self.length = length 37 | self.pitch = pitch 38 | self.angle = angle 39 | self.array_radius = (length - 1) * pitch / 2 40 | self.radius = radius 41 | # Pad name is %s%s % prefix, (index + offset) 42 | self.prefix = prefix 43 | self.offset = offset 44 | 45 | def __getitem__(self, index): 46 | pad_name = self.prefix + str(index + self.offset) 47 | pad_x = (index - 1) * self.pitch - self.array_radius 48 | return Pad(pad_name, pad_x, self.radius, self.angle) 49 | 50 | def __len__(self): 51 | return self.length 52 | 53 | def __iter__(self): 54 | self.counter = 0 55 | return self 56 | 57 | def __next__(self): 58 | if self.counter < self.length: 59 | self.counter += 1 60 | return self[self.counter] 61 | raise StopIteration() 62 | 63 | 64 | class TwoPads(object): 65 | def __init__(self, distance): 66 | self.distance = distance 67 | 68 | def __iter__(self): 69 | for i, angle in enumerate([90, -90]): 70 | for pad in PadArray(1, 0, self.distance / 2, angle, offset=i): 71 | yield pad 72 | 73 | 74 | class Sot23Pads(object): 75 | def __init__(self, c, e): 76 | self.c = c 77 | self.e = e 78 | 79 | def __iter__(self): 80 | for pad in PadArray(2, self.e * 2, self.c / 2, 0): 81 | yield pad 82 | for pad in PadArray(1, 0, self.c / 2, 180, offset=2): 83 | yield pad 84 | 85 | 86 | class DualPads(object): 87 | def __init__(self, num, pitch, radius): 88 | assert num % 2 == 0 89 | self.num = num 90 | self.pitch = pitch 91 | self.radius = radius 92 | 93 | def __iter__(self): 94 | pad_array_length = int(self.num / 2) 95 | for i, angle in enumerate([90, -90]): 96 | for pad in PadArray(pad_array_length, self.pitch, self.radius, angle, 97 | offset=pad_array_length * i): 98 | yield pad 99 | 100 | 101 | class QuadPads(object): 102 | def __init__(self, num, pitch, radius, thermal_pad=False): 103 | assert num % 4 == 0 104 | self.num = num 105 | self.pitch = pitch 106 | self.radius = radius 107 | self.thermal_pad = thermal_pad 108 | 109 | def __iter__(self): 110 | pad_array_length = int(self.num / 4) 111 | for i, angle in enumerate([90, 0, -90, 180]): 112 | for pad in PadArray(pad_array_length, self.pitch, self.radius, angle, 113 | offset=pad_array_length * i): 114 | yield pad 115 | if self.thermal_pad: 116 | yield Pad(str(self.num + 1), 0, 0, 117 | size=(self.thermal_pad, self.thermal_pad)) 118 | 119 | 120 | class GridPads(object): 121 | def __init__(self, width, length, pitch): 122 | self.width = width 123 | self.length = length 124 | self.pitch = pitch 125 | 126 | def __iter__(self): 127 | grid_radius = (self.width - 1) * self.pitch / 2 128 | prefix = GridPads.default_prefix_generator() 129 | for col in range(self.width): 130 | radius = col * self.pitch - grid_radius 131 | for pad in PadArray(self.length, self.pitch, radius, 0, 132 | prefix=next(prefix)): 133 | yield pad 134 | 135 | @staticmethod 136 | def default_prefix_generator(): 137 | for i in range(27): 138 | for j in range(27): 139 | for k in range(26): 140 | label = chr(k + ord('A')) 141 | if j > 0: 142 | label = chr(j + ord('A') - 1) + label 143 | if i > 0: 144 | label = chr(i + ord('A') - 1) + label 145 | yield label 146 | 147 | 148 | class StaggeredGridPads(object): 149 | def __init__(self, width, length, pitch): 150 | assert num % 2 == 1 151 | self.width = width 152 | self.length = length 153 | self.pitch = pitch 154 | self.grid = GridPads(width, length, pitch) 155 | 156 | def __iter__(self): 157 | for i, pad in enumerate(self.grid): 158 | if i % 2 == 1: 159 | continue 160 | yield pad 161 | 162 | 163 | class PerimeterPads(object): 164 | def __init__(self, width, length, pitch, perimeter, inner=0): 165 | self.width = width 166 | self.length = length 167 | self.pitch = pitch 168 | self.perimeter = perimeter 169 | self.inner = inner 170 | self.grid = GridPads(width, length, pitch) 171 | 172 | def __iter__(self): 173 | pad_iter = iter(self.grid) 174 | perimeter_end = self.perimeter - 1 175 | perimeter_start = self.num - self.perimeter 176 | inner_start = int((self.num - self.inner) / 2 - 1) 177 | inner_end = inner_start + self.inner + 1 178 | for col in range(self.num): 179 | for row in range(self.num): 180 | pad = next(pad_iter) 181 | if row > inner_start and row < inner_end and \ 182 | col > inner_start and col < inner_end: 183 | yield pad 184 | else: 185 | if row > perimeter_end and row < perimeter_start and \ 186 | col > perimeter_end and col < perimeter_start: 187 | continue 188 | yield pad 189 | 190 | 191 | class Courtyard(object): 192 | # IPC grid element is 0.5mm * 0.5mm 193 | IPC_GRID_SCALE = 0.5 194 | 195 | def __init__(self, coords): 196 | self.coords = coords 197 | self.polygon = Polygon(coords) 198 | self.bounds = self.polygon.bounds 199 | self.width = self.bounds[2] - self.bounds[0] 200 | self.height = self.bounds[3] - self.bounds[1] 201 | self.ipc_width = int(math.ceil(self.width / self.IPC_GRID_SCALE)) 202 | self.ipc_height = int(math.ceil(self.height / self.IPC_GRID_SCALE)) 203 | 204 | def __repr__(self): 205 | return repr(self.coords) 206 | 207 | def __str__(self): 208 | return 'width=%s (0.5mm), height=%s (0.5mm)' % \ 209 | (self.ipc_width, self.ipc_height) 210 | 211 | 212 | class RectCrtyd(Courtyard): 213 | def __init__(self, xlen, ylen): 214 | self.xlen = xlen 215 | self.ylen = ylen 216 | 217 | x, y = xlen / 2, ylen / 2 218 | super().__init__([(-x, y), (x, y), (x, -y), (-x, -y)]) 219 | 220 | 221 | class IPCGrid(RectCrtyd): 222 | 223 | def __init__(self, ipc_x, ipc_z): 224 | super().__init__(ipc_z * self.IPC_GRID_SCALE, 225 | ipc_x * self.IPC_GRID_SCALE) 226 | 227 | # X is the ylen axis 228 | self.ipc_x = ipc_x 229 | # Z is the xlen axis 230 | self.ipc_z = ipc_z 231 | 232 | 233 | class Package(object): 234 | '''Package represents a IC package. Packages are registered when imported 235 | from library.''' 236 | 237 | packages = [] 238 | 239 | def __init__(self, name, courtyard, pads, 240 | package_size=(5, 5), pad_size=(1, 1), 241 | pad_shape='rect', pad_drill=None): 242 | self.name = name 243 | self.courtyard = courtyard 244 | self.pads = [] 245 | self.package_size = package_size 246 | self.pad_size = pad_size 247 | self.pad_shape = pad_shape 248 | self.pad_drill = pad_drill 249 | 250 | for pad in pads: 251 | self.add_pad(pad) 252 | 253 | self.register_package(self) 254 | 255 | def add_pad(self, pad): 256 | pad.package = self 257 | if pad.size is None: 258 | pad.size = self.pad_size 259 | if pad.shape is None: 260 | pad.shape = self.pad_shape 261 | if pad.drill is None: 262 | pad.drill = self.pad_drill 263 | 264 | self.pads.append(pad) 265 | 266 | def pad_by_name(self, name): 267 | for pad in self.pads: 268 | if pad.name == name: 269 | return pad 270 | 271 | def size(self): 272 | bounds = self.courtyard.polygon.bounds 273 | return (bounds[2] - bounds[0], bounds[3] - bounds[1]) 274 | 275 | def to_object(self): 276 | pads = {} 277 | for pad in self.pads: 278 | pads.update(pad.to_object()) 279 | 280 | return { 281 | self.name: { 282 | 'width': self.courtyard.ipc_width, 283 | 'height': self.courtyard.ipc_height, 284 | 'pads': pads, 285 | } 286 | } 287 | 288 | def __str__(self): 289 | '''Returns the name of the Package.''' 290 | 291 | return self.name 292 | 293 | def __repr__(self): 294 | '''Returns the representation of a Package.''' 295 | 296 | pad_string = '\n'.join([2 * ' ' + repr(pad) for pad in self.pads]) 297 | return '%s %s\n%s\n' \ 298 | % (self.name, str(self.courtyard), pad_string) 299 | 300 | @classmethod 301 | def package_by_name(cls, name): 302 | '''Returns the Package with name from registered packages.''' 303 | 304 | for pkg in cls.packages: 305 | if pkg.name == name: 306 | return pkg 307 | 308 | raise IndexError('No Package with name ' + name) 309 | 310 | @classmethod 311 | def register_package(cls, package): 312 | '''Register a Package.''' 313 | 314 | try: 315 | cls.package_by_name(package.name) 316 | raise Exception( 317 | 'Package with name %s already exists' % package.name) 318 | except IndexError: 319 | cls.packages.append(package) 320 | --------------------------------------------------------------------------------