├── .gitignore
├── LICENSE
├── README.md
├── generate_zip.sh
├── metadata.json
├── plugins
├── Connect_Nets.py
├── Get_Distance.py
├── Get_PCB_Elements.py
├── Get_PCB_Stackup.py
├── Get_Parasitic.py
├── Get_Self_Inductance.py
├── Plot_PCB.py
├── __init__.py
├── icon_small.png
├── impedance.py
├── kicad_advanced
├── ngspyce
│ ├── __init__.py
│ ├── ngspyce.py
│ └── sharedspice.py
├── plugin.json
├── requirements.txt
├── s_expression_parse.py
└── sexpdata.py
└── resources
└── icon.png
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | ItemList.py
3 | preversion
4 | *.zip
5 | .vscode
6 | *.net
7 | test*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KiCad-Parasitics
2 |
3 | Plugin to analyze the wires in the PCB editor. To use the plugin, two points must be marked on the board. This is best two pads that are connected with a wire. The tool then determines the DC resistance between the two points. A parasitic inductance wired also estimated. In future versions, the parasitic capacitance to the ground plane will be determined.
4 |
5 | # Example
6 |
7 | First install Kicad-Parasitic from the Kicad "Plugin and Content Manager", then:
8 | - open Kicad
9 | - go to File -> Open Demo Project ...
10 | - select Stickhub folder
11 | - select StickHub.kicad_pro
12 | 
13 | - open StickHub.kicad_pcb
14 | - zoom in to the front layer, close to the USB connector
15 | - select the two D- vias
16 | - press on the "parasitic" icon
17 | - 
18 |
19 |
20 |
21 | # Tested until now
22 |
23 | Operating systems
24 | - [x] Windows
25 | - [x] Linux
26 | - [ ] Mac
27 |
28 | KiCad versions
29 | - [x] KiCad 7
30 | - [x] KiCad 8
31 | - [x] KiCad 9
32 |
--------------------------------------------------------------------------------
/generate_zip.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | rm KiCad-Parasitics.zip
4 | mv metadata.json metadata_.json
5 | jq --arg today "$(date +%Y.%m.%d)" '.versions[0].version |= $today' metadata_.json > metadata.json
6 |
7 | git ls-files -- 'metadata.json' 'resources*.png' 'plugins*.png' 'plugins*.py' | xargs zip KiCad-Parasitics.zip
8 | mv metadata_.json metadata.json
--------------------------------------------------------------------------------
/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://go.kicad.org/pcm/schemas/v1",
3 | "name": "Parasitics",
4 | "description": "Plugin to analyze the wires in the PCB editor.",
5 | "description_full": "Plugin to analyze the wires in the PCB editor.",
6 | "identifier": "com.github.Steffen-W.KiCad-Parasitics",
7 | "type": "plugin",
8 | "author": {
9 | "name": "Steffen Wittemeier",
10 | "contact": {
11 | "github": "https://github.com/Steffen-W"
12 | }
13 | },
14 | "maintainer": {
15 | "name": "Steffen Wittemeier",
16 | "contact": {
17 | "github": "https://github.com/Steffen-W"
18 | }
19 | },
20 | "license": "GPL-3.0",
21 | "resources": {
22 | "homepage": "https://github.com/Steffen-W/parasitic"
23 | },
24 | "tags": [
25 | "postlayoutsimulation",
26 | "inductor",
27 | "resistor",
28 | "capacitor"
29 | ],
30 | "versions": [
31 | {
32 | "version": "2025.xx.xx",
33 | "status": "stable",
34 | "kicad_version": "8.00"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/plugins/Connect_Nets.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | # Return values for error handling
4 | OK = 0
5 | NotYetConnected = 0
6 | ErrorConnection = -1
7 |
8 |
9 | def Connect_Nets(data):
10 | """
11 | This function connects networks in a KiCad-like data format.
12 | """
13 |
14 | # Initialization: Ensure missing keys exist to prevent KeyError
15 | for uuid, d in data.items():
16 | data[uuid].setdefault("Layer", [])
17 | data[uuid].setdefault("netStart", defaultdict(lambda: NotYetConnected))
18 | data[uuid].setdefault("netEnd", defaultdict(lambda: NotYetConnected))
19 | # data[uuid].setdefault("connStart", [])
20 | # data[uuid].setdefault("connEnd", [])
21 |
22 | def getNet(uuid, conn_uuid, layer, pos: tuple = (0, 0)):
23 | """
24 | Retrieves the network connection for a given UUID and layer.
25 | """
26 | if layer not in data[uuid].get("Layer", []):
27 | return ErrorConnection # Error if layer is missing
28 |
29 | temp = NotYetConnected
30 |
31 | if data[uuid]["type"] == "WIRE" and data[conn_uuid]["type"] == "WIRE":
32 | if pos == data[uuid].get("Start", (0, 0)):
33 | temp = data[uuid]["netStart"].get(layer, NotYetConnected)
34 | if temp > NotYetConnected:
35 | return temp
36 | if pos == data[uuid].get("End", (0, 0)):
37 | temp = data[uuid]["netEnd"].get(layer, NotYetConnected)
38 | else:
39 | if conn_uuid in data[uuid].get("connStart", []):
40 | temp = data[uuid]["netStart"].get(layer, NotYetConnected)
41 | if temp > NotYetConnected:
42 | return temp
43 | if conn_uuid in data[uuid].get("connEnd", []):
44 | return data[uuid]["netEnd"].get(layer, NotYetConnected)
45 |
46 | return temp
47 |
48 | def setNet(uuid, conn_uuid, layer, newNet, pos: tuple = (0, 0)):
49 | """
50 | Sets a network connection for a given UUID.
51 | """
52 | if layer not in data[uuid].get("Layer", []):
53 | return ErrorConnection # Error if layer is missing
54 |
55 | if data[uuid]["type"] == "WIRE" and data[conn_uuid]["type"] == "WIRE":
56 | if pos == data[uuid].get("Start", (0, 0)):
57 | data[uuid]["netStart"][layer] = newNet
58 | if pos == data[uuid].get("End", (0, 0)):
59 | data[uuid]["netEnd"][layer] = newNet
60 | else:
61 | if conn_uuid in data[uuid].get("connStart", []):
62 | data[uuid]["netStart"][layer] = newNet
63 | if conn_uuid in data[uuid].get("connEnd", []):
64 | data[uuid]["netEnd"][layer] = newNet
65 |
66 | return OK
67 |
68 | # Connecting networks
69 | nodeCounter = 0
70 |
71 | # First pass: Start network connections
72 | for uuid, d in data.items():
73 | for layer in data[uuid]["Layer"]:
74 | if d["netStart"].get(layer, NotYetConnected) > NotYetConnected:
75 | continue
76 |
77 | pos = d.get("Start", (0, 0))
78 | tempNet = NotYetConnected
79 |
80 | for conn in d.get("connStart", []):
81 | tmp = getNet(conn, uuid, layer, pos)
82 | if tmp > NotYetConnected:
83 | tempNet = tmp
84 |
85 | if tempNet == NotYetConnected:
86 | nodeCounter += 1
87 | tempNet = nodeCounter
88 |
89 | for conn in d.get("connStart", []):
90 | setNet(conn, uuid, layer, tempNet, pos)
91 |
92 | data[uuid]["netStart"][layer] = tempNet
93 |
94 | # Second pass: End network connections
95 | for uuid, d in data.items():
96 | for layer in data[uuid]["Layer"]:
97 | if d["netEnd"].get(layer, NotYetConnected) > NotYetConnected:
98 | continue
99 |
100 | pos = d.get("End", (0, 0))
101 | tempNet = NotYetConnected
102 | for conn in d.get("connEnd", []):
103 | tmp = getNet(conn, uuid, layer, pos)
104 | if tmp > NotYetConnected:
105 | tempNet = tmp
106 | if tempNet == NotYetConnected:
107 | nodeCounter += 1
108 | tempNet = nodeCounter
109 |
110 | for conn in d.get("connEnd", []):
111 | setNet(conn, uuid, layer, tempNet, pos)
112 | data[uuid]["netEnd"][layer] = tempNet
113 |
114 | return data
115 |
--------------------------------------------------------------------------------
/plugins/Get_Distance.py:
--------------------------------------------------------------------------------
1 | import heapq
2 | import math
3 |
4 |
5 | def dijkstra(graph, start_node):
6 | distance = {node: float("inf") for node in graph}
7 | predecessor = {node: None for node in graph}
8 | distance[start_node] = 0
9 | queue = [(0, start_node)]
10 | while queue:
11 | current_distance, current_node = heapq.heappop(queue)
12 |
13 | for neighbor, weight in graph[current_node].items():
14 | new_distance = current_distance + weight
15 | if new_distance < distance[neighbor]:
16 | distance[neighbor] = new_distance
17 | predecessor[neighbor] = current_node
18 | heapq.heappush(queue, (new_distance, neighbor))
19 | return distance, predecessor
20 |
21 |
22 | def find_shortest_path(graph, start_node, end_node):
23 | distance, predecessor_from_start1 = dijkstra(graph, start_node)
24 | if math.isinf(distance[end_node]):
25 | return None # No path found
26 | path = [end_node]
27 | while path[-1] != start_node:
28 | path.append(predecessor_from_start1[path[-1]])
29 | return distance[end_node], path[::-1]
30 |
31 |
32 | def get_graph_from_edges(edges: list):
33 | res = {}
34 | nodes = []
35 | for e in edges:
36 | res[(e[0], e[1])] = e[2]
37 | res[(e[1], e[0])] = e[2]
38 | nodes.append(e[0])
39 | nodes.append(e[1])
40 | nodes = sorted(set(nodes))
41 |
42 | edges = [(k[0], k[1], res[k]) for k in res.keys()]
43 |
44 | # Converting to a graph representation
45 | graph = {node: {} for node in nodes}
46 | for edge in edges:
47 | graph[edge[0]][edge[1]] = edge[2]
48 | return graph
49 |
50 |
51 | if __name__ == "__main__":
52 | start_node = 1
53 | end_node = 5
54 |
55 | edges = [
56 | (1, 2, 1), # (node1, node2, distance)
57 | (1, 3, 4),
58 | (2, 4, 2),
59 | (3, 4, 6),
60 | (3, 5, 3),
61 | (6, 4, 1),
62 | (5, 3, 2),
63 | ]
64 |
65 | graph = get_graph_from_edges()
66 | distance, path = find_shortest_path(graph, start_node, end_node)
67 | if path:
68 | print(
69 | f"Shortest path {str(distance)} from {str(start_node)} to {str(end_node)}: {str(path)}"
70 | )
71 | else:
72 | print(f"No path found from {str(start_node)} to {str(end_node)}")
73 |
--------------------------------------------------------------------------------
/plugins/Get_PCB_Elements.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pcbnew
3 |
4 |
5 | ToMM = pcbnew.ToMM
6 |
7 |
8 | def SaveDictToFile(dict_name, filename):
9 | with open(filename, "w") as f:
10 | f.write("data = {\n")
11 | for uuid, d in list(dict_name.items()):
12 | f.write(str(uuid))
13 | f.write(":")
14 | f.write(str(d))
15 | f.write(",\n")
16 | f.write("}")
17 |
18 |
19 | # Überprüfe, ob der Punkt sich innerhalb des Polygons befindet
20 | def IsPointInPolygon(point_, polygon_):
21 | point = np.array(point_)
22 | polygon = np.array(polygon_)
23 |
24 | n = len(polygon)
25 | inside = False
26 |
27 | p1x, p1y = polygon[0]
28 | for i in range(n + 1):
29 | p2x, p2y = polygon[i % n]
30 | if point[1] > min(p1y, p2y):
31 | if point[1] <= max(p1y, p2y):
32 | if point[0] <= max(p1x, p2x):
33 | if p1y != p2y:
34 | x_intersect = (point[1] - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
35 | if p1x == p2x or point[0] <= x_intersect:
36 | inside = not inside
37 | p1x, p1y = p2x, p2y
38 |
39 | return inside
40 |
41 |
42 | def getHash(obj: pcbnew.EDA_ITEM):
43 | return obj.m_Uuid.Hash()
44 |
45 |
46 | def getHashList(objlist):
47 | return [getHash(obj) for obj in objlist]
48 |
49 |
50 | def getPolygon(obj: pcbnew.PAD):
51 | try:
52 | poly_obj = obj.GetEffectivePolygon()
53 | except:
54 | poly_obj = obj.GetEffectivePolygon(aLayer=0) # TODO correct layer
55 | Polygon = [ToMM(poly_obj.CVertex(p)) for p in range(poly_obj.FullPointCount())]
56 | return Polygon
57 |
58 |
59 | def getLayer(obj: pcbnew.BOARD_ITEM, PossibleLayer=set([0, 31])):
60 | return sorted(set(obj.GetLayerSet().CuStack()) & PossibleLayer)
61 |
62 |
63 | def getConnections(track: pcbnew.PCB_TRACK, connect: pcbnew.CONNECTIVITY_DATA):
64 | def getVectorLen(vector):
65 | return np.sqrt(vector.dot(vector))
66 |
67 | def getDistance(point1, point2):
68 | return getVectorLen(np.array(point2) - np.array(point1))
69 |
70 | def MoveToObjCenter(wirePos, width, objPos):
71 | objPos = np.array(objPos)
72 | wirePos = np.array(wirePos)
73 |
74 | diffVector = objPos - wirePos
75 |
76 | x = np.sign(diffVector[0]) * min([abs(diffVector[0]), width / 2])
77 | y = np.sign(diffVector[1]) * min([abs(diffVector[1]), width / 2])
78 | return wirePos + np.array([x, y])
79 |
80 | ConnStart = []
81 | ConnEnd = []
82 |
83 | Start = ToMM(track.GetStart())
84 | End = ToMM(track.GetEnd())
85 |
86 | for con in connect.GetConnectedTracks(track):
87 | if type(con) is pcbnew.PCB_VIA:
88 | print(ToMM(con.GetWidth()))
89 | print(ToMM(con.GetPosition()))
90 | elif type(con) is pcbnew.PCB_TRACK:
91 | conStart = ToMM(con.GetStart())
92 | conEnd = ToMM(con.GetEnd())
93 | if Start == conStart:
94 | ConnStart.append(getHash(con))
95 | if Start == conEnd:
96 | ConnStart.append(getHash(con))
97 | if End == conStart:
98 | ConnEnd.append(getHash(con))
99 | if End == conEnd:
100 | ConnEnd.append(getHash(con))
101 |
102 | if getHash(con) not in ConnStart + ConnEnd:
103 | distance = [
104 | getDistance(Start, conStart),
105 | getDistance(Start, conEnd),
106 | getDistance(End, conStart),
107 | getDistance(End, conEnd),
108 | ]
109 | minDis = min(distance)
110 |
111 | if distance[0] == minDis or distance[1] == minDis:
112 | ConnStart.append(getHash(con))
113 | else:
114 | ConnEnd.append(getHash(con))
115 |
116 | for con in connect.GetConnectedPads(track):
117 | Polygon = getPolygon(con)
118 | Start_ = MoveToObjCenter(Start, ToMM(track.GetWidth()), ToMM(con.GetPosition()))
119 | End_ = MoveToObjCenter(End, ToMM(track.GetWidth()), ToMM(con.GetPosition()))
120 |
121 | if IsPointInPolygon(Start_, Polygon):
122 | ConnStart.append(getHash(con))
123 | if IsPointInPolygon(End_, Polygon):
124 | ConnEnd.append(getHash(con))
125 |
126 | return ConnStart, ConnEnd
127 |
128 |
129 | def Get_PCB_Elements(board: pcbnew.BOARD, connect: pcbnew.CONNECTIVITY_DATA):
130 | DesignSettings: pcbnew.BOARD_DESIGN_SETTINGS = board.GetDesignSettings()
131 | PossibleLayer = set(DesignSettings.GetEnabledLayers().CuStack())
132 | BoardThickness = ToMM(DesignSettings.GetBoardThickness())
133 |
134 | # print(f"BoardThickness {BoardThickness}mm")
135 | # print("GetTracks", len(board.GetTracks()))
136 | # print("GetAreaCount", board.GetAreaCount())
137 | # print("GetPads", len(board.GetPads()))
138 | # print("AllConnectedItems", len(board.AllConnectedItems()))
139 | # print("GetFootprints", len(board.GetFootprints()))
140 | # print("GetDrawings", len(board.GetDrawings()))
141 | # print("GetAllNetClasses", len(board.GetAllNetClasses()))
142 |
143 | ItemList = {}
144 |
145 | for track in board.GetTracks():
146 | temp = {"Layer": getLayer(track, PossibleLayer)}
147 | if type(track) is pcbnew.PCB_VIA:
148 | temp["type"] = "VIA"
149 | temp["Position"] = ToMM(track.GetStart())
150 | temp["Drill"] = ToMM(track.GetDrill())
151 | temp["Width"] = ToMM(track.GetWidth())
152 | temp["connStart"] = sorted(
153 | getHashList(connect.GetConnectedPads(track))
154 | + getHashList(connect.GetConnectedTracks(track))
155 | )
156 | temp["Area"] = 0
157 | elif type(track) is pcbnew.PCB_TRACK:
158 | temp["type"] = "WIRE"
159 | temp["Start"] = ToMM(track.GetStart())
160 | temp["End"] = ToMM(track.GetEnd())
161 | temp["Width"] = ToMM(track.GetWidth())
162 | temp["Length"] = ToMM(track.GetLength())
163 | temp["Area"] = temp["Width"] * temp["Length"]
164 | if track.GetLength() == 0:
165 | continue
166 | temp["Layer"] = [track.GetLayer()]
167 | temp["connStart"], temp["connEnd"] = getConnections(track, connect)
168 | elif type(track) is pcbnew.PCB_ARC:
169 | temp["type"] = "WIRE"
170 | temp["Start"] = ToMM(track.GetStart())
171 | temp["End"] = ToMM(track.GetEnd())
172 | temp["Radius"] = ToMM(track.GetRadius())
173 | temp["Width"] = ToMM(track.GetWidth())
174 | temp["Length"] = ToMM(track.GetLength())
175 | temp["Area"] = temp["Width"] * temp["Length"]
176 | if track.GetLength() == 0:
177 | continue
178 | temp["Layer"] = [track.GetLayer()]
179 | temp["connStart"], temp["connEnd"] = getConnections(track, connect)
180 | else:
181 | print("type", type(track), "is not considered!")
182 | continue
183 |
184 | temp["Netname"] = track.GetNetname()
185 | temp["NetCode"] = track.GetNetCode()
186 | temp["id"] = getHash(track)
187 | temp["IsSelected"] = track.IsSelected()
188 | ItemList[temp["id"]] = temp
189 |
190 | for item in board.AllConnectedItems():
191 | temp = {"Layer": getLayer(item, PossibleLayer)}
192 | if type(item) is pcbnew.PAD:
193 | temp["type"] = "PAD"
194 | temp["Shape"] = item.GetShape()
195 | # temp["PadAttr"] = Pad.ShowPadAttr()
196 | # temp["IsFlipped"] = Pad.IsFlipped()
197 | temp["Position"] = ToMM(item.GetPosition())
198 | temp["Size"] = ToMM(item.GetSize())
199 | temp["Orientation"] = item.GetOrientation().AsDegrees()
200 | temp["DrillSize"] = ToMM(item.GetDrillSize())
201 | temp["Drill"] = temp["DrillSize"][0]
202 | Layers = temp.get("Layer", [])
203 |
204 | if len(Layers):
205 | try:
206 | poly_obj = item.GetEffectivePolygon()
207 | except:
208 | poly_obj = item.GetEffectivePolygon(aLayer=Layers[0])
209 |
210 | temp["Area"] = ToMM(ToMM(poly_obj.Area()))
211 | else:
212 | temp["Area"] = 0
213 |
214 | temp["PadName"] = item.GetPadName()
215 | # temp["FootprintUUID"] = getHash(Pad.GetParent())
216 | # if Pad.GetParent():
217 | # temp["FootprintReference"] = Pad.GetParent().GetReference()
218 |
219 | elif type(item) is pcbnew.ZONE:
220 | # pcbnew.ZONE().GetZoneName
221 | if "teardrop" in item.GetZoneName():
222 | continue
223 | temp["type"] = "ZONE"
224 | temp["Position"] = ToMM(item.GetPosition())
225 | temp["Area"] = ToMM(ToMM(item.GetFilledArea()))
226 | temp["NumCorners"] = item.GetNumCorners()
227 | temp["ZoneName"] = item.GetZoneName()
228 | elif type(item) is pcbnew.PCB_TRACK:
229 | continue # already in board.GetTracks()
230 | elif type(item) is pcbnew.BOARD_CONNECTED_ITEM:
231 | if item.GetNetCode() == 0:
232 | continue
233 | print("type", type(item), "is not considered!")
234 | continue
235 | else:
236 | print("type", type(item), "is not considered!")
237 | continue
238 |
239 | temp["Netname"] = item.GetNetname()
240 | temp["NetCode"] = item.GetNetCode()
241 | temp["id"] = getHash(item)
242 | temp["IsSelected"] = item.IsSelected()
243 | temp["connStart"] = sorted(
244 | getHashList(connect.GetConnectedPads(item))
245 | + getHashList(connect.GetConnectedTracks(item))
246 | )
247 | ItemList[temp["id"]] = temp
248 |
249 | for uuid, d in list(ItemList.items()): # TODO: WIRES still need to be considered
250 | if d["type"] == "ZONE":
251 | for item in d["connStart"]:
252 | if not "connEND" in ItemList[item]:
253 | ItemList[item]["connStart"].append(uuid)
254 |
255 | return ItemList
256 |
--------------------------------------------------------------------------------
/plugins/Get_PCB_Stackup.py:
--------------------------------------------------------------------------------
1 | from os.path import exists
2 |
3 | try:
4 | from .s_expression_parse import parse_sexp
5 | except:
6 | from s_expression_parse import parse_sexp
7 |
8 | import re
9 |
10 |
11 | def extract_layer_from_string_old(input_string): # kicad <9.0
12 | if input_string == "F.Cu":
13 | return 0
14 | elif input_string == "B.Cu":
15 | return 31
16 | else:
17 | match = re.search(r"In(\d+)\.Cu", input_string)
18 | if match:
19 | return int(match.group(1))
20 | return None
21 |
22 |
23 | def extract_layer_from_string(input_string): # kicad >=9.0
24 | # https://gitlab.com/kicad/code/kicad/-/commit/5e0abadb23425765e164f49ee2f893e94ddb97fc
25 | if input_string == "F.Cu":
26 | return 0
27 | elif input_string == "B.Cu":
28 | return 2
29 | else:
30 | match = re.match(r"In(\d+)\.Cu", input_string)
31 | if match:
32 | inner_index = int(match.group(1))
33 | return 2 * inner_index + 2 # In1_Cu = 4, In2_Cu = 6, ...
34 | return None
35 |
36 |
37 | def search_recursive(line: list, entry: str, all=False):
38 | if type(line[0]) == str and line[0] == entry:
39 | if all:
40 | return line
41 | else:
42 | return line[1]
43 |
44 | for e in line:
45 | if type(e) == list:
46 | res = search_recursive(line=e, entry=entry, all=all)
47 | if not res == None:
48 | return res
49 | return None
50 |
51 |
52 | def Get_PCB_Stackup_fun(ProjectPath="./test.kicad_pcb", new_v9=True):
53 | def readFile2var(path):
54 | if not exists(path):
55 | return None
56 |
57 | with open(path, "r") as file:
58 | data = file.read()
59 | return data
60 |
61 | PhysicalLayerStack = []
62 | CuStack = {}
63 | try:
64 | if exists(ProjectPath):
65 | txt = readFile2var(ProjectPath)
66 | parsed = parse_sexp(txt)
67 |
68 | while True:
69 | setup = search_recursive(parsed, "setup", all=True)
70 | if not setup:
71 | break
72 |
73 | stackup = search_recursive(setup, "stackup", all=True)
74 | if not stackup:
75 | break
76 |
77 | abs_height = 0.0
78 | for layer in stackup:
79 | tmp = {}
80 | tmp["layer"] = search_recursive(layer, "layer")
81 | tmp["thickness"] = search_recursive(layer, "thickness")
82 | tmp["epsilon_r"] = search_recursive(layer, "epsilon_r")
83 | tmp["type"] = search_recursive(layer, "type")
84 |
85 | if not tmp["thickness"] == None:
86 | if new_v9:
87 | tmp["cu_layer"] = extract_layer_from_string(tmp["layer"])
88 | else:
89 | tmp["cu_layer"] = extract_layer_from_string_old(
90 | tmp["layer"]
91 | )
92 | tmp["abs_height"] = abs_height
93 | abs_height += float(tmp["thickness"])
94 | PhysicalLayerStack.append(tmp)
95 | break
96 |
97 | for Layer in PhysicalLayerStack:
98 | if not Layer["cu_layer"] == None:
99 | CuStack[Layer["cu_layer"]] = {
100 | "thickness": Layer["thickness"],
101 | "name": Layer["layer"],
102 | "abs_height": Layer["abs_height"],
103 | }
104 | if Layer["thickness"] <= 0:
105 | raise Exception("Problematic layer thickness detected")
106 | except:
107 | print("ERROR: Reading the CuStack")
108 |
109 | if not CuStack:
110 | layers = search_recursive(parsed, "layers", all=True)
111 | for layer in layers:
112 | if type(layer) == list and "signal" in layer:
113 | CuStack[layer[0]] = {
114 | "thickness": 0.035,
115 | "name": layer[1],
116 | "abs_height": float(layer[0]) / 20, # arbitrary assumption
117 | }
118 | print("estimated CuStack", CuStack)
119 |
120 | return PhysicalLayerStack, CuStack
121 |
--------------------------------------------------------------------------------
/plugins/Get_Parasitic.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import os
3 | import math
4 | import traceback
5 |
6 |
7 | try:
8 | if __name__ == "__main__":
9 | from Get_Self_Inductance import calculate_self_inductance, interpolate_vertices
10 | from Get_Distance import find_shortest_path, get_graph_from_edges
11 | import ngspyce
12 | else:
13 | from .Get_Self_Inductance import calculate_self_inductance, interpolate_vertices
14 | from .Get_Distance import find_shortest_path, get_graph_from_edges
15 | from . import ngspyce
16 | except Exception as e:
17 | print(traceback.format_exc())
18 |
19 |
20 | def round_n(n, decimals=0):
21 | if math.isinf(n):
22 | return n
23 | multiplier = 10**decimals
24 | return math.floor(n * multiplier + 0.5) / multiplier
25 |
26 |
27 | def RunSimulation(resistors, conn1, conn2):
28 | # https://github.com/ignamv/ngspyce/
29 | filename = os.path.join(os.path.dirname(__file__), "TempNetlist.net")
30 |
31 | Rshunt = 0.1
32 | with open(filename, "w") as f:
33 | f.write("* gnetlist -g spice-sdb\n")
34 |
35 | for i, res in enumerate(resistors):
36 | entry = "R{} {} {} {:.10f}\n".format(i + 1, res[0], res[1], res[2])
37 | f.write(entry)
38 |
39 | f.write("v1 {} 0 1\n".format(conn1))
40 | f.write("R{} 0 {} {}\n".format(i + 2, conn2, Rshunt))
41 | f.write(".end")
42 |
43 | ngspyce.source(filename)
44 | ngspyce.dc("v1", 1, 1, 1) # set v1 to 1V
45 | os.remove(filename)
46 | vout = ngspyce.vector(str(conn2))[0]
47 |
48 | if not vout == 0:
49 | R = (1 - vout) / (vout / Rshunt)
50 | else:
51 | R = -1
52 | return R
53 |
54 |
55 | # in https://www.youtube.com/watch?v=hNHTwpegFBw
56 | # rho_cu = 1/47e6 # Ohm * m # 26% more than 1.68e-8
57 |
58 | rho_cu = 1.68e-8 # Ohm * m
59 |
60 |
61 | def calcResWIRE(Length, Width, cu_thickness=0.035, freq=0):
62 | # https://learnemc.com/EXT/calculators/Resistance_Calculator/rect.html
63 |
64 | if freq == 0:
65 | return Length * rho_cu / (cu_thickness * Width) * 1000.0
66 | else: # TODO
67 | # mu = 1
68 | # SkinDepth = 1 / np.sqrt(freq * np.pi * mu / rho_cu) # in m
69 | return Length * rho_cu / (cu_thickness * Width) * 1000.0
70 |
71 |
72 | def calcResVIA(Drill, Length, cu_thickness=0.035):
73 | radius = Drill / 2
74 | area = np.pi * ((radius + cu_thickness) ** 2 - radius**2)
75 | return Length * rho_cu / area * 1000
76 |
77 |
78 | def Get_shortest_path_RES(path, resistors):
79 | def get_res(x1, x2):
80 | x = next(x for x in resistors if {x1, x2} == set(x[0:2]))
81 | return x[2]
82 |
83 | RES = 0
84 | for i in range(1, len(path)):
85 | RES += get_res(path[i - 1], path[i])
86 |
87 | return RES
88 |
89 |
90 | def Get_Parasitic(data, CuStack, conn1, conn2, netcode):
91 | resistors = []
92 | coordinates = {}
93 |
94 | Area = {l: 0 for l in range(32)} # for all layer
95 |
96 | for uuid, d in data.items():
97 | if not netcode == d["NetCode"]:
98 | continue
99 |
100 | if len(d["Layer"]) > 1:
101 | for i in range(1, len(d["Layer"])):
102 | Layer1 = d["Layer"][i - 1]
103 | Layer2 = d["Layer"][i]
104 | thickness = CuStack[0]["thickness"] # from Layer Top
105 | if Layer2 in CuStack and Layer1 in CuStack:
106 | distance = abs(
107 | CuStack[Layer2]["abs_height"] - CuStack[Layer1]["abs_height"]
108 | )
109 | else:
110 | print("ERROR: CuStack is incomplete!")
111 | print("Layer", d["Layer"])
112 | print(CuStack)
113 | continue
114 | if "Drill" not in d:
115 | continue
116 | resistor = calcResVIA(d["Drill"], distance, cu_thickness=thickness)
117 | if resistor < 0:
118 | raise ValueError("Error in resistance calculation!")
119 | resistors.append(
120 | [d["netStart"][Layer1], d["netStart"][Layer2], resistor, distance]
121 | )
122 | coordinates[d["netStart"][Layer1]] = (
123 | d["Position"][0],
124 | d["Position"][1],
125 | CuStack[Layer1]["abs_height"],
126 | )
127 | coordinates[d["netStart"][Layer2]] = (
128 | d["Position"][0],
129 | d["Position"][1],
130 | CuStack[Layer2]["abs_height"],
131 | )
132 |
133 | else:
134 | Layer = d["Layer"][0]
135 | Area[Layer] += d["Area"]
136 | if d["type"] == "WIRE":
137 | netStart = d["netStart"][Layer]
138 | netEnd = d["netEnd"][Layer]
139 | thickness = CuStack[Layer]["thickness"]
140 | resistor = calcResWIRE(d["Length"], d["Width"], cu_thickness=thickness)
141 | if resistor < 0:
142 | raise ValueError("Error in resistance calculation!")
143 | resistors.append([netStart, netEnd, resistor, d["Length"]])
144 |
145 | coordinates[d["netStart"][Layer]] = (
146 | d["Start"][0],
147 | d["Start"][1],
148 | CuStack[Layer]["abs_height"],
149 | )
150 | coordinates[d["netEnd"][Layer]] = (
151 | d["End"][0],
152 | d["End"][1],
153 | CuStack[Layer]["abs_height"],
154 | )
155 |
156 | Area_reduc = {l: Area[l] for l in Area.keys() if Area[l] > 0}
157 |
158 | for res in resistors:
159 | if res[2] <= 0:
160 | raise ValueError("Error in resistance calculation!")
161 |
162 | # edges = list( (node1, node2, distance) )
163 | edges = [(i[0], i[1], i[3]) for i in resistors]
164 | graph = get_graph_from_edges(edges)
165 | try:
166 | Distance, path = find_shortest_path(graph, conn1, conn2)
167 | path3d = [coordinates[p] for p in path]
168 | short_path_RES = Get_shortest_path_RES(path, resistors)
169 | except Exception as e:
170 | short_path_RES = -1
171 | Distance, path3d = float("inf"), []
172 | print(traceback.format_exc())
173 | print("ERROR in find_shortest_path")
174 |
175 | inductance_nH = 0
176 | try:
177 | if len(path3d) > 2:
178 | vertices = interpolate_vertices(path3d, num_points=1000)
179 | inductance_nH = 0 # calculate_self_inductance(vertices, current=1) * 1e9
180 | except Exception as e:
181 | inductance_nH = 0
182 | print(traceback.format_exc())
183 | print("ERROR in calculate_self_inductance")
184 |
185 | try:
186 | Resistance = RunSimulation(resistors, conn1, conn2)
187 | except Exception as e:
188 | Resistance = -1
189 | print(traceback.format_exc())
190 | print("ERROR in RunSimulation")
191 | return Resistance, Distance, inductance_nH, short_path_RES, Area_reduc
192 |
--------------------------------------------------------------------------------
/plugins/Get_Self_Inductance.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | def calculate_self_inductance(vertices, current): # TODO Must be checked in more detail
5 | """
6 | Berechnet die Selbstinduktivität einer polygonalen Spule.
7 |
8 | Args:
9 | vertices (list of tuples): Eine Liste von Eckpunkten der Spule [(x1, y1, z1), (x2, y2, z2), ...].
10 | current (float): Der Strom, der durch die Spule fließt.
11 |
12 | Returns:
13 | float: Selbstinduktivität der Spule in Henry (H).
14 | """
15 | mu_0 = 4 * np.pi * 1e-7 # Magnetische Permeabilität des Vakuums
16 |
17 | total_inductance = 0.0
18 |
19 | for i in range(len(vertices)):
20 | p1 = np.array(vertices[i])
21 | p2 = np.array(vertices[(i + 1) % len(vertices)]) # Schließe den Kreis
22 |
23 | # Vektor vom Punkt p1 zum Punkt p2
24 | delta = p2 - p1
25 |
26 | # Berechne den Mittelpunkt zwischen p1 und p2
27 | midpoint = (p1 + p2) / 2.0
28 |
29 | # Berechne den Abstand vom Mittelpunkt zum Ursprung
30 | r = np.linalg.norm(midpoint)
31 |
32 | # Berechne die Selbstinduktivität dieses Leiterstücks
33 | dL = np.linalg.norm(delta) # Länge des Leiterstücks
34 | dL_hat = delta / dL # Einheitsvektor in Richtung des Stroms
35 |
36 | contribution = mu_0 * current * dL / (4 * np.pi * r) # Beitrag zum Gesamtfeld
37 |
38 | total_inductance += contribution
39 |
40 | return total_inductance
41 |
42 |
43 | def interpolate_vertices(vertices, num_points):
44 | """
45 | Interpoliert zwischen den gegebenen Eckpunkten, um mehr Punkte zu erzeugen.
46 |
47 | Args:
48 | vertices (list of tuples): Eine Liste von Eckpunkten der Spule [(x1, y1, z1), (x2, y2, z2), ...].
49 | num_points (int): Die gewünschte Anzahl von interpolierten Punkten zwischen den Eckpunkten.
50 |
51 | Returns:
52 | list of tuples: Eine Liste von interpolierten Punkten.
53 | """
54 | interpolated_points = []
55 |
56 | for i in range(len(vertices)):
57 | p1 = np.array(vertices[i])
58 | p2 = np.array(vertices[(i + 1) % len(vertices)]) # Schließe den Kreis
59 |
60 | for j in range(num_points):
61 | # Lineare Interpolation zwischen p1 und p2
62 | t = j / float(num_points)
63 | interpolated_point = tuple(p1 + t * (p2 - p1))
64 | interpolated_points.append(interpolated_point)
65 |
66 | return interpolated_points
67 |
68 |
69 | if __name__ == "__main__":
70 | import matplotlib.pyplot as plt
71 |
72 | # Beispielaufruf:
73 | vertices = [
74 | (0, 0, 0),
75 | (2, 0, 0),
76 | (2, 2, 0),
77 | (0, 2, 0),
78 | ] # Beispiel-Eckpunkte einer quadratischen Spule
79 | current = 1.0 # Beispielstrom in Ampere
80 |
81 | num_points = 1000 # Beispielanzahl von interpolierten Punkten
82 | vertices = interpolate_vertices(vertices, num_points)
83 |
84 | xpoints = [i[0] for i in vertices]
85 | ypoints = [i[1] for i in vertices]
86 | plt.plot(xpoints, ypoints)
87 | plt.show()
88 |
89 | inductance = calculate_self_inductance(vertices, current) * 1000 * 1000
90 | print(f"Die Selbstinduktivität der Spule beträgt {inductance:.6f} uHenry (H).")
91 |
--------------------------------------------------------------------------------
/plugins/Plot_PCB.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | from matplotlib.patches import Rectangle, Ellipse
3 | from matplotlib.transforms import Affine2D
4 | import numpy as np
5 |
6 |
7 | def Plot_PCB(data):
8 | figure, axes = plt.subplots()
9 | axes.set_aspect(1)
10 | axes.invert_yaxis()
11 |
12 | shape = {0: "Kreis", 1: "Oval", 2: "Rechteck"}
13 |
14 | Color = {0: "red", 1: "green", 2: "orange", 3: "cyan", 4: "pink", 31: "blue"}
15 | for i in range(0, 32):
16 | if i not in Color:
17 | Color[i] = "silver"
18 |
19 | def NameNetInPlot(uuid, layer, active=True, netStart=True):
20 | if netStart:
21 | text = data[uuid]["netStart"]
22 | if "Start" in data[uuid]:
23 | pos = data[uuid]["Start"]
24 | else:
25 | pos = data[uuid]["Position"]
26 | else:
27 | text = data[uuid]["netEnd"]
28 | pos = data[uuid]["End"]
29 | if layer == 0:
30 | plt.text(
31 | *pos,
32 | text[layer],
33 | horizontalalignment="left",
34 | verticalalignment="bottom",
35 | )
36 | elif layer == 31:
37 | plt.text(
38 | *pos, text[layer], horizontalalignment="left", verticalalignment="top"
39 | )
40 | elif layer == 1:
41 | plt.text(
42 | *pos,
43 | text[layer],
44 | horizontalalignment="right",
45 | verticalalignment="bottom",
46 | )
47 | else:
48 | plt.text(
49 | *pos, text[layer], horizontalalignment="right", verticalalignment="top"
50 | )
51 |
52 | for uuid, d in list(data.items()):
53 | if d["type"] == "VIA":
54 | circ = plt.Circle(d["Position"], d["Width"] / 2, color="grey", alpha=0.5)
55 | axes.add_artist(circ)
56 | if "Drill" in d:
57 | axes.add_artist(plt.Circle(d["Position"], d["Drill"] / 2, color="w"))
58 | # plt.text(*d["Position"], str(data[uuid]["netStart"]))
59 | for l in d["Layer"]:
60 | NameNetInPlot(uuid, l)
61 |
62 | def plotwire(Start, End, Width, layer, uuid):
63 | plt.arrow(
64 | Start[0],
65 | Start[1],
66 | End[0] - Start[0],
67 | End[1] - Start[1],
68 | width=Width,
69 | head_length=0,
70 | head_width=Width,
71 | color=Color[layer],
72 | alpha=0.5,
73 | )
74 | axes.add_artist(plt.Circle(Start, Width / 2, color=Color[layer], alpha=0.25))
75 | axes.add_artist(plt.Circle(End, Width / 2, color=Color[layer], alpha=0.25))
76 | NameNetInPlot(uuid, layer, netStart=True)
77 | NameNetInPlot(uuid, layer, netStart=False)
78 |
79 | for uuid, d in list(data.items()):
80 | if d["type"] == "WIRE":
81 | plotwire(d["Start"], d["End"], d["Width"], d["Layer"][0], uuid)
82 | # data[uuid]["R"] = calcResWIRE(d["Start"], d["End"], d["Width"])
83 |
84 | for uuid, d in list(data.items()):
85 | if d["type"] == "PAD":
86 | if d["Shape"] in {0, 2}: # oval
87 | ellip = Ellipse(
88 | d["Position"],
89 | *d["Size"],
90 | color=Color[d["Layer"][0]],
91 | alpha=0.5,
92 | angle=d["Orientation"],
93 | )
94 | axes.add_patch(ellip)
95 | else:
96 | rec = plt.Rectangle(
97 | np.array(d["Position"]) - np.array(d["Size"]) / 2,
98 | width=d["Size"][0],
99 | height=d["Size"][1],
100 | color=Color[d["Layer"][0]],
101 | alpha=0.5,
102 | transform=Affine2D().rotate_deg_around(
103 | *d["Position"], d["Orientation"]
104 | )
105 | + axes.transData,
106 | )
107 | axes.add_patch(rec)
108 | NameNetInPlot(uuid, d["Layer"][0])
109 |
110 | plt.show()
111 |
--------------------------------------------------------------------------------
/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | import pcbnew
2 | import os.path
3 | import wx
4 | import traceback
5 | from pathlib import Path
6 | import math
7 |
8 | try:
9 | if not __name__ == "__main__":
10 | from .Get_PCB_Elements import Get_PCB_Elements, SaveDictToFile
11 | from .Connect_Nets import Connect_Nets
12 | from .Get_PCB_Stackup import Get_PCB_Stackup_fun
13 | from .Get_Parasitic import Get_Parasitic
14 | except Exception as e:
15 | print(traceback.format_exc())
16 |
17 |
18 | class KiCadPluginParasitic(pcbnew.ActionPlugin):
19 | def defaults(self):
20 | self.name = "parasitic"
21 | self.category = "parasitic"
22 | self.description = "parasitic"
23 | self.show_toolbar_button = True
24 | self.plugin_path = os.path.dirname(__file__)
25 | self.icon_file_name = os.path.join(self.plugin_path, "icon_small.png")
26 | self.dark_icon_file_name = os.path.join(self.plugin_path, "icon_small.png")
27 |
28 | def Run(self):
29 | try:
30 | debug = 0
31 |
32 | board = pcbnew.GetBoard()
33 | connect = board.GetConnectivity()
34 | Settings = pcbnew.GetSettingsManager()
35 |
36 | # KiCad_CommonSettings = Settings.GetCommonSettings()
37 | KiCad_UserSettingsPath = Settings.GetUserSettingsPath()
38 | KiCad_SettingsVersion = str(Settings.GetSettingsVersion())
39 | try:
40 | new_v9 = int(KiCad_SettingsVersion.split(".")[0]) >= 9
41 | except:
42 | print("KiCad_SettingsVersion", KiCad_SettingsVersion)
43 | new_v9 = True
44 | board_FileName = Path(board.GetFileName())
45 |
46 | ####################################################
47 | # Get PCB Elements
48 | ####################################################
49 |
50 | ItemList = Get_PCB_Elements(board, connect)
51 |
52 | ####################################################
53 | # save Variable ItemList to file (for debug)
54 | ####################################################
55 |
56 | if debug:
57 | save_as_file = os.path.join(self.plugin_path, "ItemList.py")
58 | print("save_as_file", save_as_file)
59 | SaveDictToFile(ItemList, save_as_file)
60 | with open(save_as_file, "a") as f:
61 | f.write('\nboard_FileName = "')
62 | f.write(str(board_FileName))
63 | f.write('"')
64 |
65 | ####################################################
66 | # connect nets together
67 | ####################################################
68 |
69 | data = Connect_Nets(ItemList)
70 | if debug:
71 | pprint(data)
72 |
73 | ####################################################
74 | # read PhysicalLayerStack from file
75 | ####################################################
76 |
77 | PhysicalLayerStack, CuStack = Get_PCB_Stackup_fun(
78 | ProjectPath=board_FileName, new_v9=new_v9
79 | )
80 | if debug:
81 | pprint(CuStack)
82 |
83 | ####################################################
84 | # get resistance
85 | ####################################################
86 |
87 | Selected = [d for uuid, d in data.items() if d["IsSelected"]]
88 |
89 | message = ""
90 | if len(Selected) == 2:
91 | conn1 = Selected[0]["netStart"][Selected[0]["Layer"][0]]
92 | conn2 = Selected[1]["netStart"][Selected[1]["Layer"][0]]
93 | NetCode = Selected[0]["NetCode"]
94 | if not NetCode == Selected[1]["NetCode"]:
95 | message = "The marked points are not in the same network."
96 | else:
97 | message = "You have to mark exactly two elements."
98 | message += " Preferably pads or vias."
99 |
100 | if message == "":
101 | (
102 | Resistance,
103 | Distance,
104 | inductance_nH,
105 | short_path_RES,
106 | Area,
107 | ) = Get_Parasitic(data, CuStack, conn1, conn2, NetCode)
108 |
109 | message += "\nShortest distance between the two points ≈ "
110 | message += "{:.3f} mm".format(Distance)
111 |
112 | message += "\n"
113 | if not PhysicalLayerStack:
114 | message += "\nNo Physical Stackup could be found!"
115 | if short_path_RES > 0:
116 | message += "\nResistance (only short path) ≈ "
117 | message += "{:.3f} mOhm".format(short_path_RES * 1000)
118 | elif short_path_RES == 0:
119 | message += "\nResistance (only short path) ≈ "
120 | message += "{:.3f} mOhm".format(short_path_RES * 1000)
121 | message += "\nSurfaces of the zones are considered perfectly "
122 | message += "conductive and short-circuit points. This is probably the case here."
123 | else:
124 | message += "\nNo connection was found between the two marked points"
125 |
126 | if not math.isinf(Resistance) and Resistance >= 0:
127 | message += "\nResistance between both points ≈ "
128 | message += "{:.3f} mOhm".format(Resistance * 1000)
129 | elif Resistance < 0:
130 | message += "\nERROR in Resistance Network calculation."
131 | message += " Probably no ngspice installation could be found."
132 | message += " The result about the short path"
133 | message += " path is however uninfluenced."
134 | else:
135 | message += "\nNo connection was found between the two marked points"
136 |
137 | # message += "\n"
138 | # if inductance_nH > 0:
139 | # message += "\nThe determined self-inductance ≈ "
140 | # message += "{:.3f} nH".format(inductance_nH)
141 | # message += "\nHere it was assumed that the line is free without ground planes."
142 | # message += "\nThe result is to be taken with special caution!"
143 | # else:
144 | # message += "\nThe determined self-inductance ≈ NAN"
145 | # message += "\nFor direct and uninterrupted connections the calculation is not applicable."
146 |
147 | message += "\n"
148 | if len(Area) > 0:
149 | message += "\nRough area estimation of the signal"
150 | message += " (without zones and vias):"
151 | for layer in Area.keys():
152 | message += "\nLayer {}: {:.3f} mm², {} μm copper".format(
153 | CuStack[layer]["name"],
154 | Area[layer],
155 | CuStack[layer]["thickness"] * 1000,
156 | )
157 |
158 | dlg = wx.MessageDialog(
159 | None,
160 | message,
161 | "Analysis result",
162 | wx.OK,
163 | )
164 | dlg.ShowModal()
165 | dlg.Destroy()
166 |
167 | ####################################################
168 | # print pcb in matplotlib
169 | ####################################################
170 |
171 | # if debug:
172 | # from Plot_PCB import Plot_PCB
173 | # Plot_PCB(data)
174 |
175 | except Exception as e:
176 | dlg = wx.MessageDialog(
177 | None,
178 | traceback.format_exc(),
179 | "Fatal Error",
180 | wx.OK | wx.ICON_ERROR,
181 | )
182 | dlg.ShowModal()
183 | dlg.Destroy()
184 |
185 |
186 | if not __name__ == "__main__":
187 | KiCadPluginParasitic().register()
188 |
189 |
190 | if __name__ == "__main__":
191 | from ItemList import data, board_FileName # instead: import Get_PCB_Elements
192 | from Connect_Nets import Connect_Nets
193 | from Get_PCB_Stackup import Get_PCB_Stackup_fun
194 | from Get_Parasitic import Get_Parasitic
195 | from Plot_PCB import Plot_PCB
196 | from pprint import pprint
197 |
198 | # Get PCB Elements
199 | ItemList = data
200 |
201 | # connect nets together
202 | data = Connect_Nets(ItemList)
203 | # pprint(data)
204 |
205 | # read PhysicalLayerStack from file
206 | PhysicalLayerStack, CuStack = Get_PCB_Stackup_fun(ProjectPath=board_FileName)
207 | pprint(CuStack)
208 |
209 | # get resistance
210 | Selected = [d for uuid, d in list(data.items()) if d["IsSelected"]]
211 | if len(Selected) == 2:
212 | conn1 = Selected[0]["netStart"][Selected[0]["Layer"][0]]
213 | conn2 = Selected[1]["netStart"][Selected[1]["Layer"][0]]
214 | NetCode = Selected[0]["NetCode"]
215 | if not NetCode == Selected[1]["NetCode"]:
216 | print("The marked points are not in the same network.")
217 |
218 | Resistance, Distance, inductance_nH, short_path_RES, Area = Get_Parasitic(
219 | data, CuStack, conn1, conn2, NetCode
220 | )
221 | print(f"Distance {Distance} mm")
222 | print(f"Resistance {Resistance} mOhm")
223 | print(f"Resistance {short_path_RES} mOhm (only short path)")
224 | print(f"inductance {inductance_nH} nH")
225 | # print(f"Area {Area} mm2")
226 |
227 | if len(Area) > 0:
228 | for layer in Area.keys():
229 | txt = "Layer {}: {:.3f} mm²".format(layer, Area[layer])
230 | print(txt)
231 | else:
232 | print("You have to mark exactly two elements.")
233 |
234 | # print pcb in matplotlib
235 | # Plot_PCB(data)
236 |
--------------------------------------------------------------------------------
/plugins/icon_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steffen-W/KiCad-Parasitics/e2dc5647b86c2316b10a04626640105d2dc302b4/plugins/icon_small.png
--------------------------------------------------------------------------------
/plugins/impedance.py:
--------------------------------------------------------------------------------
1 | from math import log, pow, pi, sqrt
2 |
3 | v_0 = 299792458 # m/s
4 | epsilon_0 = 1e-9 / (36 * pi)
5 |
6 |
7 | def get_Microstrip_Cap(w: float, h: float, l: float, epsilon_r: float):
8 | # https://www.emisoftware.com/calculator/microstrip-capacitance/
9 |
10 | if w < h:
11 | C = epsilon_r * l / (60 * v_0 * log(8 * h / w + w / 4 / h))
12 | else:
13 | C = (
14 | (epsilon_r * l)
15 | * (w / h + 1.393 + 0.667 * log(w / h + 1.444))
16 | / (120 * pi * v_0)
17 | )
18 | return C
19 |
20 |
21 | def get_Microstrip_Z0(w: float, h: float, l: float, epsilon_r: float):
22 | # https://www.everythingrf.com/rf-calculators/microstrip-calculator
23 | wh = w / h
24 |
25 | if wh < 1:
26 | eps_e = (epsilon_r + 1) / 2 + (epsilon_r - 1) / 2 * (
27 | 1 / sqrt(1 + 12 / wh) + 0.4 * (1 - wh) * (1 - wh)
28 | )
29 | Z0 = 60 / sqrt(eps_e) * log(8 / wh + 0.25 * wh)
30 | else:
31 | eps_e = (epsilon_r + 1) / 2 + (epsilon_r - 1) / (2 * sqrt(1 + 12 / wh))
32 | Z0 = 120 * pi / (sqrt(eps_e) * (wh + 1.393 + 2 / 3 * log(wh + 1.444)))
33 | return Z0
34 |
35 |
36 | def get_Plate_Cap(w: float, h: float, l: float, epsilon_r: float):
37 | C = epsilon_0 * epsilon_r * w * l / h
38 | return C
39 |
40 |
41 | def get_Coplanar_Cap(w: float, gap: float, l: float, epsilon_r: float):
42 | # https://www.emisoftware.com/calculator/coplanar-capacitance/
43 |
44 | s = gap / (gap + 2 * w)
45 | if gap <= 1 / sqrt(2):
46 | x = pow(1 - s * s, 1 / 4)
47 | C = (epsilon_r * l) * log(-2 / (x - 1) * (x + 1)) / (377 * pi * v_0)
48 | else:
49 | C = (epsilon_r * l) / (120 * v_0 * log(-2 / (sqrt(s) - 1)) * (sqrt(s) + 1))
50 | return C
51 |
52 |
53 | def get_Stripline_Cap(w: float, h: float, l: float, epsilon_r: float):
54 | ws = w / (h / 2)
55 | factor = epsilon_r * l / (30 * pi * v_0)
56 |
57 | if ws >= 0.35:
58 | C = factor * (ws + 0.441)
59 | else:
60 | C = factor * (ws - (0.35 - ws) * (0.35 - ws) + 0.441)
61 | return C
62 |
63 |
64 | if __name__ == "__main__":
65 | w = 0.4 * 1e-3 # Width in m
66 | h = 1.55 * 1e-3 # Height above ground in m
67 | l = 1000 * 1e-3 # Length in m
68 | epsilon_r = 4.6 # Relative Permittivity
69 |
70 | gap = 100 * 1e-3 # Gap in m (only Coplanar)
71 |
72 | print("Microstrip C:", get_Microstrip_Cap(w, h, l, epsilon_r) * 1e12, "pF")
73 | print("Microstrip Z0:", get_Microstrip_Z0(w, h, l, epsilon_r), "Ohm")
74 | print("Stripline C:", get_Stripline_Cap(w, h * 2, l, epsilon_r) * 1e12, "pF")
75 | print("Plate C:", get_Plate_Cap(w, h, l, epsilon_r) * 1e12, "pF")
76 | print("Coplanar C:", get_Coplanar_Cap(w, gap, l, epsilon_r) * 1e12, "pF")
77 |
--------------------------------------------------------------------------------
/plugins/kicad_advanced:
--------------------------------------------------------------------------------
1 | EnableAPILogging=0
--------------------------------------------------------------------------------
/plugins/ngspyce/__init__.py:
--------------------------------------------------------------------------------
1 | """ngspyce, a python interface to the ngspice circuit simulator"""
2 | from .ngspyce import *
3 |
--------------------------------------------------------------------------------
/plugins/ngspyce/ngspyce.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import logging
3 | from .sharedspice import *
4 |
5 | __all__ = [
6 | 'cmd',
7 | 'circ',
8 | 'plots',
9 | 'vector_names',
10 | 'vectors',
11 | 'vector',
12 | 'try_float',
13 | 'model_parameters',
14 | 'device_state',
15 | 'alter_model',
16 | 'ac',
17 | 'dc',
18 | 'operating_point',
19 | 'linear_sweep',
20 | 'save',
21 | 'destroy',
22 | 'decibel',
23 | 'alter',
24 | 'alterparams',
25 | 'source',
26 | 'xspice_enabled'
27 | ]
28 |
29 | logger = logging.getLogger(__name__)
30 | logging.basicConfig(level=logging.WARNING)
31 |
32 |
33 | def initialize():
34 | spice.ngSpice_Init(printfcn, statfcn, controlled_exit, send_data, None, None,
35 | None)
36 | # Prevent paging output of commands (hangs)
37 | cmd('set nomoremode')
38 |
39 |
40 | def cmd(command):
41 | """
42 | Send a command to the ngspice engine
43 |
44 | Parameters
45 | ----------
46 | command : str
47 | An ngspice command
48 |
49 | Returns
50 | -------
51 | list of str
52 | Lines of the captured output
53 |
54 | Examples
55 | --------
56 |
57 | Print all default variables
58 |
59 | >>> ns.cmd('print all')
60 | ['false = 0.000000e+00',
61 | 'true = 1.000000e+00',
62 | 'boltz = 1.380620e-23',
63 | 'c = 2.997925e+08',
64 | 'e = 2.718282e+00',
65 | 'echarge = 1.602190e-19',
66 | 'i = 0.000000e+00,1.000000e+00',
67 | 'kelvin = -2.73150e+02',
68 | 'no = 0.000000e+00',
69 | 'pi = 3.141593e+00',
70 | 'planck = 6.626200e-34',
71 | 'yes = 1.000000e+00']
72 |
73 | """
74 | max_length = 1023
75 | if len(command) > max_length:
76 | raise ValueError('Command length', len(command), 'greater than',
77 | max_length)
78 | del captured_output[:]
79 | spice.ngSpice_Command(command.encode('ascii'))
80 | logger.debug('Command %s returned %s', command, captured_output)
81 | return captured_output
82 |
83 |
84 | def circ(netlist_lines):
85 | """
86 | Load a netlist
87 |
88 | Parameters
89 | ----------
90 |
91 | netlist_lines : str or list of str
92 | Netlist, either as a list of lines, or a
93 | single multi-line string. Indentation and white
94 | space don't matter. Unlike a netlist file, the
95 | first line doesn't need to be a comment, and you
96 | don't need to provide the `.end`.
97 |
98 | Returns
99 | -------
100 | int
101 | `1` upon error, otherwise `0`.
102 |
103 | Examples
104 | --------
105 |
106 | Using a sequence of lines:
107 |
108 | >>> ns.circ(['va a 0 dc 1', 'r a 0 2'])
109 | 0
110 |
111 | Using a single string:
112 |
113 | >>> ns.circ('''va a 0 dc 1
114 | ... r a 0 2''')
115 | 0
116 |
117 | """
118 | if issubclass(type(netlist_lines), str):
119 | netlist_lines = netlist_lines.split('\n')
120 | netlist_lines = [line.encode('ascii') for line in netlist_lines]
121 | # First line is ignored by the engine
122 | netlist_lines.insert(0, b'* ngspyce-created netlist')
123 | # Add netlist end
124 | netlist_lines.append(b'.end')
125 | # Add list terminator
126 | netlist_lines.append(None)
127 | array = (c_char_p * len(netlist_lines))(*netlist_lines)
128 | return spice.ngSpice_Circ(array)
129 |
130 |
131 | def plots():
132 | """
133 | List available plots (result sets)
134 |
135 | Each plot is a collection of vector results
136 |
137 | Returns
138 | -------
139 | list of str
140 | List of existing plot names
141 |
142 | Examples
143 | --------
144 |
145 | Each analysis creates a new plot
146 |
147 | >>> ns.circ(['v1 a 0 dc 1', 'r1 a 0 1k']); ns.plots()
148 | ['const']
149 | >>> ns.operating_point(); ns.plots()
150 | ['op1', 'const']
151 | >>> ns.dc('v1', 0, 5, 1); ns.plots()
152 | ['dc1', 'op1', 'const']
153 |
154 | Get lists of vectors available in different plots:
155 |
156 | >>> ns.vectors(plot='const').keys()
157 | dict_keys(['echarge', 'e', 'TRUE', 'FALSE', 'no', 'i', ... 'c', 'boltz'])
158 | >>> ns.vectors(plot='ac1').keys()
159 | dict_keys(['V(1)', 'vout', 'v1#branch', 'frequency'])
160 | """
161 | ret = []
162 | plotlist = spice.ngSpice_AllPlots()
163 | ii = 0
164 | while True:
165 | if not plotlist[ii]:
166 | return ret
167 | ret.append(plotlist[ii].decode('ascii'))
168 | ii += 1
169 |
170 |
171 | def vector_names(plot=None):
172 | """
173 | Names of vectors present in the specified plot
174 |
175 | Names of the voltages, currents, etc present in the specified plot.
176 | Defaults to the current plot.
177 |
178 | Parameters
179 | ----------
180 | plot : str, optional
181 | Plot name. Defaults to the current plot.
182 |
183 | Returns
184 | -------
185 | list of str
186 | Names of vectors in the plot
187 |
188 | Examples
189 | --------
190 |
191 | List built-in constants
192 |
193 | >>> ns.vector_names('const')
194 | ['planck', 'boltz', 'echarge', 'kelvin', 'i', 'c', 'e', 'pi', 'FALSE', 'no', 'TRUE', 'yes']
195 |
196 | Vectors produced by last analysis
197 |
198 | >>> ns.circ('v1 a 0 dc 2');
199 | >>> ns.operating_point();
200 | >>> ns.vector_names()
201 | ['v1#branch', 'a']
202 |
203 | """
204 | names = []
205 | if plot is None:
206 | plot = spice.ngSpice_CurPlot().decode('ascii')
207 | veclist = spice.ngSpice_AllVecs(plot.encode('ascii'))
208 | ii = 0
209 | while True:
210 | if not veclist[ii]:
211 | return names
212 | names.append(veclist[ii].decode('ascii'))
213 | ii += 1
214 |
215 |
216 | def vectors(names=None):
217 | """
218 | Dictionary with the specified vectors (defaults to all in current plot)
219 |
220 | Parameters
221 | ----------
222 | names : list of str, optional
223 | Names of vectors to retrieve. If omitted, return all vectors
224 | in current plot
225 |
226 | Returns
227 | -------
228 | dict from str to ndarray
229 | Dictionary of vectors. Keys are vector names and values are Numpy
230 | arrays containing the data.
231 |
232 | Examples
233 | --------
234 |
235 | Do an AC sweep and retrieve the frequency axis and output voltage
236 |
237 | >>> nc.ac('dec', 3, 1e3, 10e6);
238 | >>> nc.ac_results = vectors(['frequency', 'vout'])
239 |
240 | """
241 | if names is None:
242 | names = vector_names()
243 | return dict(zip(names, map(vector, names)))
244 |
245 |
246 | def vector(name, plot=None):
247 | """
248 | Return a numpy.ndarray with the specified vector
249 |
250 | Uses the current plot by default.
251 |
252 | Parameters
253 | ----------
254 | name : str
255 | Name of vector
256 | plot : str, optional
257 | Which plot the vector is in. Defaults to current plot.
258 |
259 | Returns
260 | -------
261 | ndarray
262 | Value of the vector
263 |
264 | Examples
265 | --------
266 |
267 | Run an analysis and retrieve a vector
268 |
269 | >>> ns.circ(['v1 a 0 dc 2', 'r1 a 0 1k']);
270 | >>> ns.dc('v1', 0, 2, 1);
271 | >>> ns.vector('v1#branch')
272 | array([ 0. , -0.001, -0.002])
273 |
274 | """
275 | if plot is not None:
276 | name = plot + '.' + name
277 | vec = spice.ngGet_Vec_Info(name.encode('ascii'))
278 | if not vec:
279 | raise RuntimeError('Vector {} not found'.format(name))
280 | vec = vec[0]
281 | if vec.v_length == 0:
282 | array = np.array([])
283 | elif vec.v_flags & dvec_flags.vf_real:
284 | array = np.ctypeslib.as_array(vec.v_realdata, shape=(vec.v_length,))
285 | elif vec.v_flags & dvec_flags.vf_complex:
286 | components = np.ctypeslib.as_array(vec.v_compdata,
287 | shape=(vec.v_length, 2))
288 | array = np.ndarray(shape=(vec.v_length,), dtype=np.complex128,
289 | buffer=components)
290 | else:
291 | raise RuntimeError('No valid data in vector')
292 | logger.debug('Fetched vector {} type {}'.format(name, vec.v_type))
293 | array.setflags(write=False)
294 | if name == 'frequency':
295 | return array.real
296 | return array
297 |
298 |
299 | def try_float(s):
300 | """
301 | Parse `s` as float if possible, otherwise return `s`.
302 | """
303 | try:
304 | return float(s)
305 | except ValueError:
306 | try:
307 | return float(s.replace(',', '.'))
308 | except ValueError:
309 | return s
310 |
311 |
312 | def model_parameters(device=None, model=None):
313 | """
314 | Model parameters for device or model
315 |
316 | Parameters
317 | ----------
318 | device : str, optional
319 | Instance name
320 | model : str, optional
321 | Model card name
322 |
323 | Returns
324 | -------
325 | dict from str to float or str
326 | Model parameters
327 |
328 | Examples
329 | --------
330 |
331 | Parameters of a resistor's model
332 |
333 | >>> ns.circ('r1 a 0 2k');
334 | >>> ns.model_parameters(device='r1')
335 | {'description': 'Resistor models (Simple linear resistor)', 'model': 'R',
336 | 'rsh': 0.0, 'narrow': 0.0, 'short': 0.0, 'tc1': 0.0, 'tc2': 0.0,
337 | 'tce': 0.0, 'defw': 0.0, 'l': 0.0, 'kf': 0.0, 'af': 0.0, 'r': 0.0,
338 | 'bv_max': 0.0, 'lf': 0.0, 'wf': 0.0, 'ef': 0.0}
339 | """
340 | if device is None:
341 | if model is not None:
342 | lines = cmd('showmod #' + model.lower())
343 | else:
344 | raise ValueError('Either device or model must be specified')
345 | else:
346 | if model is None:
347 | lines = cmd('showmod ' + device.lower())
348 | else:
349 | raise ValueError('Only specify one of device, model')
350 | ret = dict(description=lines.pop(0))
351 | ret.update({parts[0]: try_float(parts[1])
352 | for parts in map(str.split, lines)})
353 | return ret
354 |
355 |
356 | def device_state(device):
357 | """
358 | Dict with device state
359 |
360 | Parameters
361 | ----------
362 | device : str
363 | Instance name
364 |
365 | Returns
366 | -------
367 | dict from str to float or str
368 | Device description, model, operating point, etc.
369 |
370 | Examples
371 | --------
372 |
373 | Resistor description
374 |
375 | >>> ns.circ(['r1 a 0 4'])
376 | >>> ns.device_state('r1')
377 | {'description': 'Resistor: Simple linear resistor', 'device': 'r1',
378 | 'model': 'R', 'resistance': 4.0, 'ac': 4.0, 'dtemp': 0.0, 'bv_max': 0.0,
379 | 'noisy': 0.0}
380 | """
381 | lines = cmd('show ' + device.lower())
382 |
383 | ret = dict(description=lines.pop(0))
384 | ret.update({parts[0]: try_float(parts[1])
385 | for parts in map(str.split, lines)})
386 | return ret
387 |
388 |
389 | def alter_model(model, **params):
390 | """
391 | Change parameters of a model card
392 |
393 | Parameters
394 | ----------
395 | model : str
396 | Model card name
397 | """
398 | for k, v in params.items():
399 | cmd('altermod {} {} = {:.6e}'.format(model, k, v))
400 |
401 |
402 | def ac(mode, npoints, fstart, fstop):
403 | """
404 | Small-signal AC analysis
405 |
406 | Parameters
407 | ----------
408 | mode : {'lin', 'oct', 'dec'}
409 | Frequency axis spacing: linear, octave or decade
410 | npoints : int
411 | If mode is ``'lin'``, this is the total number of points for the sweep.
412 | Otherwise, this is the number of points per decade or per octave.
413 | fstart : float
414 | Starting frequency
415 | fstop : float
416 | Final frequency
417 |
418 | Returns
419 | -------
420 | dict from str to ndarray
421 | Result vectors: voltages, currents and frequency (under key ``'frequency'``).
422 |
423 | Examples
424 | --------
425 |
426 | Sweep from 1 kHz to 10 MHz with 3 points per decade
427 |
428 | >>> results = nc.ac('dec', 3, 1e3, 10e6)
429 | >>> len(results['frequency'])
430 | 13
431 |
432 | Sweep from 20 to 20 kHz in 21 linearly spaced points
433 |
434 | >>> results = nc.ac('lin', 21, 20, 20e3)
435 | >>> len(results['frequency'])
436 | 21
437 |
438 | Bode plot of low-pass filter::
439 |
440 | ns.circ('''
441 | v1 in 0 dc 0 ac 1
442 | r1 in out 1k
443 | c1 out 0 1n''')
444 | results = ns.ac('dec', 2, 1e0, 1e9)
445 | plt.semilogx(results['frequency'], 2*ns.decibel(results['out']))
446 |
447 | .. image:: lowpass.png
448 |
449 | """
450 | modes = ('dec', 'lin', 'oct')
451 | if mode.lower() not in modes:
452 | raise ValueError("'{}' is not a valid AC sweep "
453 | "mode: {}".format(mode, modes))
454 | if fstop < fstart:
455 | raise ValueError('Start frequency', fstart,
456 | 'greater than stop frequency', fstop)
457 | cmd('ac {} {} {} {}'.format(mode, npoints, fstart, fstop))
458 | return vectors()
459 |
460 |
461 | def group(iterable, grouplength):
462 | return zip(*(iterable[ii::grouplength]
463 | for ii in range(grouplength)))
464 |
465 |
466 | def dc(*sweeps):
467 | """
468 | Analyze DC transfer function, return vectors with one axis per sweep
469 |
470 | Parameters
471 | ----------
472 | sweeps:
473 | One or two sequences of (src, start, stop, increment).
474 | src can be an independent voltage or current source, a resistor, or ``'TEMP'``.
475 |
476 | Returns
477 | -------
478 | dict from str to ndarray
479 | Voltages and currents. If there is a secondary sweep, the ndarrays will have two axes.
480 |
481 | Examples
482 | --------
483 |
484 | Sweep a voltage source
485 |
486 | >>> ns.circ('v1 a 0 dc 0');
487 | >>> ns.dc('v1', 0, 5, 1)
488 | {'a': array([ 0., 1., 2., 3., 4., 5.]),
489 | 'v-sweep': array([ 0., 1., 2., 3., 4., 5.]),
490 | 'v1': array([0, 1, 2, 3, 4, 5]),
491 | 'v1#branch': array([ 0., 0., 0., 0., 0., 0.])}
492 |
493 | Add a secondary sweep::
494 |
495 | ns.circ(['v1 a 0 dc 0', 'r1 a 0 1k'])
496 | results = ns.dc('v1', 0, 3, 1, 'r1', 1e3, 10e3, 1e3)
497 | plt.plot(-results['v1#branch']);
498 |
499 | .. image:: secondary_sweep.png
500 |
501 | """
502 | # TODO: support more than two sweeps
503 | # TODO: implement other sweeps
504 | cmd('dc ' + ' '.join(map(str, sweeps)))
505 | sweepvalues = [linear_sweep(*sweep[1:])
506 | for sweep in group(sweeps, 4)]
507 | sweeplengths = tuple(map(len, sweepvalues))
508 | ret = {k: v.reshape(sweeplengths, order='F')
509 | for k, v in vectors().items()}
510 | # Add vectors with swept sources/parameters
511 | for ii, (name, values) in enumerate(zip(sweeps[::4], sweepvalues)):
512 | shape = [length if ii == jj else 1
513 | for jj, length in enumerate(sweeplengths)]
514 | ret[name] = values.reshape(shape, order='F')
515 | return ret
516 |
517 |
518 | def operating_point():
519 | """
520 | Analyze DC operating point
521 |
522 | Returns
523 | -------
524 | dict from str to ndarray
525 | Voltages and currents
526 | """
527 | cmd('op')
528 | return vectors()
529 |
530 |
531 | def save(vector_name):
532 | """
533 | Save this vector in the following analyses
534 |
535 | If this command is used, only explicitly saved vectors will be kept in next analysis.
536 |
537 | Parameters
538 | ----------
539 | vector_name : str
540 | Name of the vector
541 | """
542 | cmd('save ' + vector_name)
543 |
544 |
545 | def destroy(plotname='all'):
546 | """
547 | Erase plot from memory
548 |
549 | Parameters
550 | ----------
551 | plotname : str, optional
552 | Name of a plot. If omitted, erase all plots.
553 | """
554 | cmd('destroy ' + plotname)
555 |
556 |
557 | def decibel(x):
558 | '''Calculate 10*log(abs(x))'''
559 | return 10. * np.log10(np.abs(x))
560 |
561 |
562 | def alter(device, **parameters):
563 | """
564 | Alter device parameters
565 |
566 | Parameters
567 | ----------
568 | device : str
569 | Instance name
570 |
571 | Examples
572 | --------
573 |
574 | >>> ns.alter('R1', resistance=200)
575 | >>> ns.alter('vin', ac=2, dc=3)
576 | """
577 | for k, v in parameters.items():
578 | if not isinstance(v, (list, tuple)):
579 | v = str(v)
580 | else:
581 | v = '[ ' + ' '.join(v) + ' ]'
582 | cmd('alter {} {} = {}'.format(device.lower(), k, v))
583 |
584 |
585 | def alterparams(**kwargs):
586 | for k, v in kwargs.items():
587 | cmd('alterparam {} = {}'.format(k, v))
588 | cmd('reset')
589 |
590 |
591 | def linear_sweep(start, stop, step):
592 | """
593 | Numbers from start to stop (inclusive), separated by step.
594 |
595 | These match the values used in a dc linear sweep
596 |
597 | Returns
598 | -------
599 | ndarray
600 |
601 | Examples
602 | --------
603 |
604 | >>> ns.linear_sweep(0, 100, 20)
605 | array([ 0, 20, 40, 60, 80, 100])
606 |
607 | """
608 | if (start > stop and step > 0) or (start < stop and step < 0):
609 | raise ValueError("Can't sweep from", start, 'to', stop, 'with step',
610 | step)
611 | ret = []
612 | nextval = start
613 | while True:
614 | if np.sign(step) * nextval - np.sign(step) * stop >= (
615 | np.finfo(float).eps * 1e3):
616 | return np.array(ret)
617 | ret.append(nextval)
618 | nextval = nextval + step
619 |
620 |
621 | def source(filename):
622 | """
623 | Evaluate a ngspice input file
624 |
625 | This function is the same as the ngspice source command, so the first line
626 | of the file is considered a title line, lines beginning with the character
627 | ``*`` are considered comments and are ignored, etc.
628 |
629 | Parameters
630 | ----------
631 | filename : str
632 | A file containing a circuit netlist.
633 | """
634 | cmd("source '{}'".format(filename))
635 |
636 |
637 | def xspice_enabled():
638 | """
639 | Was libngspice compiled with XSpice support?
640 |
641 | Returns
642 | -------
643 | bool
644 | """
645 | return '** XSPICE extensions included' in cmd('version -f')
646 |
647 |
648 | initialize()
649 |
--------------------------------------------------------------------------------
/plugins/ngspyce/sharedspice.py:
--------------------------------------------------------------------------------
1 | import os
2 | import platform
3 | import logging
4 | from ctypes import (CDLL, CFUNCTYPE, Structure, c_int, c_char_p, c_void_p,
5 | c_bool, c_double, POINTER, c_short)
6 | from ctypes.util import find_library
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | # libngspice source code is listed before the relevant ctype structs
11 |
12 | if os.name == 'nt': # Windows
13 | # http://stackoverflow.com/a/13277363
14 | curr_dir_before = os.getcwd()
15 |
16 | drive = os.getenv("SystemDrive") or 'C:'
17 |
18 | # Python and DLL must both be same number of bits
19 | if platform.architecture()[0] == '64bit':
20 | spice_path = os.path.join(drive, os.sep, 'Spice64')
21 | elif platform.architecture()[0] == '32bit':
22 | spice_path = os.path.join(drive, os.sep, 'Spice')
23 | else:
24 | raise RuntimeError("Couldn't determine if Python is 32-bit or 64-bit")
25 |
26 | """
27 | https://sourceforge.net/p/ngspice/discussion/133842/thread/1cece652/#4e32/5ab8/9027
28 | On Windows, when environment variable SPICE_LIB_DIR is empty, ngspice
29 | looks in `C:\\Spice64\\share\\ngspice\\scripts`. If the variable is not empty
30 | it tries `%SPICE_LIB_DIR%\\scripts\\spinit`
31 | """
32 |
33 | if 'SPICE_LIB_DIR' not in os.environ:
34 | os.environ['SPICE_LIB_DIR'] = os.path.join(spice_path, 'share',
35 | 'ngspice')
36 |
37 | try:
38 | spice = CDLL('ngspice')
39 | except:
40 | os.chdir(os.path.join(spice_path, 'bin_dll'))
41 | spice = CDLL('ngspice')
42 | os.chdir(curr_dir_before)
43 | else: # Linux, etc.
44 | try:
45 | lib_location = os.environ['LIBNGSPICE']
46 | except KeyError:
47 | lib_location = find_library('ngspice')
48 |
49 | # try homebrew location as a last resort for MacOS
50 | if lib_location is None and platform.system() == "Darwin":
51 | lib_location = "/opt/homebrew/lib/libngspice.dylib"
52 |
53 | spice = CDLL(lib_location)
54 |
55 | captured_output = []
56 |
57 |
58 | @CFUNCTYPE(c_int, c_char_p, c_int, c_void_p)
59 | def printfcn(output, _id, _ret):
60 | """Callback for libngspice to print a message"""
61 | global captured_output
62 | prefix, _, content = output.decode('ascii').partition(' ')
63 | if prefix == 'stderr':
64 | logger.error(content)
65 | else:
66 | captured_output.append(content)
67 | return 0
68 |
69 |
70 | @CFUNCTYPE(c_int, c_char_p, c_int, c_void_p)
71 | def statfcn(status, _id, _ret):
72 | """
73 | Callback for libngspice to report simulation status like 'tran 5%'
74 | """
75 | logger.debug(status.decode('ascii'))
76 | return 0
77 |
78 |
79 | @CFUNCTYPE(c_int, c_int, c_bool, c_bool, c_int, c_void_p)
80 | def controlled_exit(exit_status, immediate_unloading, requested_exit,
81 | libngspice_id, ret):
82 | logger.debug('ControlledExit',
83 | dict(exit_status=exit_status,
84 | immediate_unloading=immediate_unloading,
85 | requested_exit=requested_exit,
86 | libngspice_id=libngspice_id, ret=ret))
87 |
88 |
89 | # typedef struct vecvalues {
90 | # char* name; /* name of a specific vector */
91 | # double creal; /* actual data value */
92 | # double cimag; /* actual data value */
93 | # bool is_scale;/* if 'name' is the scale vector */
94 | # bool is_complex;/* if the data are complex numbers */
95 | # } vecvalues, *pvecvalues;
96 |
97 |
98 | class vecvalues(Structure):
99 | _fields_ = [
100 | ('name', c_char_p),
101 | ('creal', c_double),
102 | ('cimag', c_double),
103 | ('is_scale', c_bool),
104 | ('is_complex', c_bool)]
105 |
106 |
107 | # typedef struct vecvaluesall {
108 | # int veccount; /* number of vectors in plot */
109 | # int vecindex; /* index of actual set of vectors. i.e. the number of accepted data points */
110 | # pvecvalues *vecsa; /* values of actual set of vectors, indexed from 0 to veccount - 1 */
111 | # } vecvaluesall, *pvecvaluesall;
112 |
113 |
114 | class vecvaluesall(Structure):
115 | _fields_ = [
116 | ('veccount', c_int),
117 | ('vecindex', c_int),
118 | ('vecsa', POINTER(POINTER(vecvalues)))]
119 |
120 |
121 | @CFUNCTYPE(c_int, POINTER(vecvaluesall), c_int, c_int, c_void_p)
122 | def send_data(vecvaluesall_, num_structs, libngspice_id, ret):
123 | logger.debug('SendData', dict(vecvaluesall=vecvaluesall_,
124 | num_structs=num_structs,
125 | libngspice_id=libngspice_id,
126 | ret=ret))
127 |
128 |
129 | # int ngSpice_Command(char* command);
130 | spice.ngSpice_Command.argtypes = [c_char_p]
131 |
132 | # int ngSpice_Circ(char**)
133 | spice.ngSpice_Circ.argtypes = [POINTER(c_char_p)]
134 | spice.ngSpice_AllPlots.restype = POINTER(c_char_p)
135 |
136 | spice.ngSpice_AllVecs.argtypes = [c_char_p]
137 | spice.ngSpice_AllVecs.restype = POINTER(c_char_p)
138 | spice.ngSpice_CurPlot.restype = c_char_p
139 |
140 |
141 | # struct ngcomplex {
142 | # double cx_real;
143 | # double cx_imag;
144 | # } ;
145 |
146 | class ngcomplex(Structure):
147 | _fields_ = [
148 | ('cx_real', c_double),
149 | ('cx_imag', c_double)]
150 |
151 |
152 | # /* Dvec flags. */
153 | # enum dvec_flags {
154 | # VF_REAL = (1 << 0), /* The data is real. */
155 | # VF_COMPLEX = (1 << 1), /* The data is complex. */
156 | # VF_ACCUM = (1 << 2), /* writedata should save this vector. */
157 | # VF_PLOT = (1 << 3), /* writedata should incrementally plot it. */
158 | # VF_PRINT = (1 << 4), /* writedata should print this vector. */
159 | # VF_MINGIVEN = (1 << 5), /* The v_minsignal value is valid. */
160 | # VF_MAXGIVEN = (1 << 6), /* The v_maxsignal value is valid. */
161 | # VF_PERMANENT = (1 << 7) /* Don't garbage collect this vector. */
162 | # };
163 |
164 |
165 | class dvec_flags(object):
166 | vf_real = (1 << 0) # The data is real.
167 | vf_complex = (1 << 1) # The data is complex.
168 | vf_accum = (1 << 2) # writedata should save this vector.
169 | vf_plot = (1 << 3) # writedata should incrementally plot it.
170 | vf_print = (1 << 4) # writedata should print this vector.
171 | vf_mingiven = (1 << 5) # The v_minsignal value is valid.
172 | vf_maxgiven = (1 << 6) # The v_maxsignal value is valid.
173 | vf_permanent = (1 << 7) # Don't garbage collect this vector.
174 |
175 |
176 | # /* vector info obtained from any vector in ngspice.dll.
177 | # Allows direct access to the ngspice internal vector structure,
178 | # as defined in include/ngspice/devc.h .*/
179 | # typedef struct vector_info {
180 | # char *v_name; /* Same as so_vname. */
181 | # int v_type; /* Same as so_vtype. */
182 | # short v_flags; /* Flags (a combination of VF_*). */
183 | # double *v_realdata; /* Real data. */
184 | # ngcomplex_t *v_compdata; /* Complex data. */
185 | # int v_length; /* Length of the vector. */
186 | # } vector_info, *pvector_info;
187 |
188 |
189 | class vector_info(Structure):
190 | _fields_ = [
191 | ('v_name', c_char_p),
192 | ('v_type', c_int),
193 | ('v_flags', c_short),
194 | ('v_realdata', POINTER(c_double)),
195 | ('v_compdata', POINTER(ngcomplex)),
196 | ('v_length', c_int)]
197 |
198 |
199 | # /* get info about a vector */
200 | # pvector_info ngGet_Vec_Info(char* vecname);
201 | spice.ngGet_Vec_Info.restype = POINTER(vector_info)
202 | spice.ngGet_Vec_Info.argtypes = [c_char_p]
203 |
204 | # Unit names for use with pint or other unit libraries
205 | vector_type = [
206 | 'dimensionless', # notype = 0
207 | 'second', # time = 1
208 | 'hertz', # frequency = 2
209 | 'volt', # voltage = 3
210 | 'ampere', # current = 4
211 | 'NotImplemented', # output_n_dens = 5
212 | 'NotImplemented', # output_noise = 6
213 | 'NotImplemented', # input_n_dens = 7
214 | 'NotImplemented', # input_noise = 8
215 | 'NotImplemented', # pole = 9
216 | 'NotImplemented', # zero = 10
217 | 'NotImplemented', # sparam = 11
218 | 'NotImplemented', # temp = 12
219 | 'ohm', # res = 13
220 | 'ohm', # impedance = 14
221 | 'siemens', # admittance = 15
222 | 'watt', # power = 16
223 | 'dimensionless' # phase = 17
224 | 'NotImplemented', # db = 18
225 | 'farad' # capacitance = 19
226 | 'coulomb' # charge = 21
227 | ]
228 |
229 |
230 | #
231 | # enum simulation_types {
232 | # ...
233 | # };
234 | class simulation_type(object):
235 | notype = 0
236 | time = 1
237 | frequency = 2
238 | voltage = 3
239 | current = 4
240 | output_n_dens = 5
241 | output_noise = 6
242 | input_n_dens = 7
243 | input_noise = 8
244 | pole = 9
245 | zero = 10
246 | sparam = 11
247 | temp = 12
248 | res = 13
249 | impedance = 14
250 | admittance = 15
251 | power = 16
252 | phase = 17
253 | db = 18
254 | capacitance = 19
255 | charge = 20
256 |
--------------------------------------------------------------------------------
/plugins/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://go.kicad.org/api/schemas/v1",
3 | "identifier": "com.github.Steffen-W.KiCad-Parasitics",
4 | "name": "Parasitics",
5 | "description": "Plugin to analyze the wires in the PCB editor.",
6 | "runtime": {
7 | "type": "python",
8 | "version": "3"
9 | },
10 | "actions": [
11 | {
12 | "identifier": "Parasitics-action",
13 | "name": "Parasitics Action",
14 | "description": "Plugin to analyze the wires in the PCB editor.",
15 | "show-button": true,
16 | "scopes": [
17 | "pcb"
18 | ],
19 | "entrypoint": "__init__.py",
20 | "icons-light": [
21 | "icon.png"
22 | ],
23 | "icons-dark": [
24 | "icon.png"
25 | ]
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/plugins/requirements.txt:
--------------------------------------------------------------------------------
1 | kicad-python
--------------------------------------------------------------------------------
/plugins/s_expression_parse.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | dbg = False
4 |
5 | term_regex = r"""(?mx)
6 | \s*(?:
7 | (?P\()|
8 | (?P\))|
9 | (?P\-?\d+\.\d+|\-?\d+)|
10 | (?P"(?:(?:\\")|[^"])*")|
11 | (?P[^(^)\s]+)
12 | )"""
13 |
14 |
15 | def parse_sexp(sexp):
16 | stack = []
17 | out = []
18 | if dbg:
19 | print("%-6s %-14s %-44s %-s" % tuple("term value out stack".split()))
20 | for termtypes in re.finditer(term_regex, sexp):
21 | term, value = [(t, v) for t, v in termtypes.groupdict().items() if v][0]
22 | if dbg:
23 | print("%-7s %-14s %-44r %-r" % (term, value, out, stack))
24 | if term == "brackl":
25 | stack.append(out)
26 | out = []
27 | elif term == "brackr":
28 | assert stack, "Trouble with nesting of brackets"
29 | tmpout, out = out, stack.pop(-1)
30 | out.append(tmpout)
31 | elif term == "num":
32 | v = float(value)
33 | if v.is_integer():
34 | v = int(v)
35 | out.append(v)
36 | elif term == "sq":
37 | out.append(value[1:-1])
38 | elif term == "s":
39 | out.append(value)
40 | else:
41 | raise NotImplementedError("Error: %r" % (term, value))
42 | assert not stack, "Trouble with nesting of brackets"
43 | return out[0]
44 |
45 |
46 | def print_sexp(exp):
47 | out = ""
48 | if type(exp) == type([]):
49 | out += "(" + " ".join(print_sexp(x) for x in exp) + ")"
50 | elif type(exp) == type("") and re.search(r"[\s()]", exp):
51 | out += '"%s"' % repr(exp)[1:-1].replace('"', '"')
52 | else:
53 | out += "%s" % exp
54 | return out
55 |
56 |
57 | if __name__ == "__main__":
58 | from pprint import pprint
59 |
60 | sexp = """(sym_lib_table
61 | (version 7)
62 | (lib (name "4xxx")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/4xxx.kicad_sym")(options "")(descr "4xxx series symbols"))
63 | (lib (name "4xxx_IEEE")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/4xxx_IEEE.kicad_sym")(options "")(descr "4xxx series IEEE symbols"))
64 | (lib (name "74xGxx")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/74xGxx.kicad_sym")(options "")(descr "74xGxx symbols"))
65 | (lib (name "74xx")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/74xx.kicad_sym")(options "")(descr "74xx symbols"))
66 | (lib (name "74xx_IEEE")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/74xx_IEEE.kicad_sym")(options "")(descr "74xx series IEEE symbols"))
67 | )"""
68 |
69 | parsed = parse_sexp(sexp)
70 | # pprint(parsed)
71 | for line in parsed:
72 | if type(line) == list and line[0] == "lib":
73 | for item in line:
74 | print(item)
75 |
--------------------------------------------------------------------------------
/plugins/sexpdata.py:
--------------------------------------------------------------------------------
1 | # [[[cog import cog; cog.outl('"""\n%s\n"""' % file('README.rst').read()) ]]]
2 | from __future__ import unicode_literals
3 | """
4 | S-expression parser for Python
5 | ==============================
6 |
7 | `sexpdata` is a simple S-expression parser/serializer. It has
8 | simple `load` and `dump` functions like `pickle`, `json` or `PyYAML`
9 | module.
10 |
11 | >>> from sexpdata import loads, dumps
12 | >>> loads('("a" "b")')
13 | ['a', 'b']
14 | >>> print(dumps(['a', 'b']))
15 | ("a" "b")
16 |
17 |
18 | You can install `sexpdata` from PyPI_::
19 |
20 | pip install sexpdata
21 |
22 |
23 | Links:
24 |
25 | * `Documentation (at Read the Docs) `_
26 | * `Repository (at GitHub) `_
27 | * `Issue tracker (at GitHub) `_
28 | * `PyPI `_
29 | * `Travis CI `_
30 |
31 |
32 | License
33 | -------
34 |
35 | `sexpdata` is licensed under the terms of the BSD 2-Clause License.
36 | See the source code for more information.
37 |
38 | """
39 | # [[[end]]]
40 |
41 | # Copyright (c) 2012 Takafumi Arakaki
42 | # All rights reserved.
43 |
44 | # Redistribution and use in source and binary forms, with or without
45 | # modification, are permitted provided that the following conditions are
46 | # met:
47 |
48 | # Redistributions of source code must retain the above copyright notice,
49 | # this list of conditions and the following disclaimer.
50 |
51 | # Redistributions in binary form must reproduce the above copyright
52 | # notice, this list of conditions and the following disclaimer in the
53 | # documentation and/or other materials provided with the distribution.
54 |
55 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
56 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
57 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
58 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
59 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
60 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
61 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
62 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
63 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
64 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
65 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
66 |
67 | __version__ = '1.0.1'
68 | __author__ = 'Joshua D. Boyd, Takafumi Arakaki'
69 | __license__ = 'BSD License'
70 | __all__ = [
71 | # API functions:
72 | 'load', 'loads', 'dump', 'dumps', 'parse',
73 | # Utility functions:
74 | 'car', 'cdr',
75 | # S-expression classes:
76 | 'Symbol', 'String', 'Quoted', 'Brackets', 'Parens',
77 | ]
78 |
79 | import re
80 | from collections import namedtuple
81 | try:
82 | from collections.abc import Iterable, Mapping, Sequence
83 | except ImportError:
84 | # Python < 3.3
85 | from collections import Iterable, Mapping, Sequence
86 | from itertools import chain
87 | from string import whitespace
88 |
89 |
90 | ### PEP fallbacks
91 |
92 | try:
93 | from functools import singledispatch
94 | except ImportError:
95 | from singledispatch import singledispatch
96 |
97 |
98 | ### Python 3 compatibility
99 |
100 | try:
101 | unicode
102 | PY3 = False
103 | except NameError:
104 | unicode = str # Python 3
105 | PY3 = True
106 |
107 |
108 | ### Interface
109 |
110 | def load(filelike, **kwds):
111 | """
112 | Load object from S-expression stored in `filelike`.
113 |
114 | :arg filelike: A text stream object.
115 |
116 | See :func:`loads` for valid keyword arguments.
117 |
118 | >>> import io
119 | >>> fp = io.StringIO()
120 | >>> sexp = [Symbol('a'), Symbol('b')] # let's dump and load this object
121 | >>> dump(sexp, fp)
122 | >>> _ = fp.seek(0)
123 | >>> load(fp) == sexp
124 | True
125 |
126 | """
127 | return loads(filelike.read(), **kwds)
128 |
129 |
130 | def loads(string, **kwds):
131 | """
132 | Load object from S-expression `string`.
133 |
134 | :arg string: String containing an S-expression.
135 | :type nil: str or None
136 | :keyword nil: A symbol interpreted as an empty list.
137 | Default is ``'nil'``.
138 | :type true: str or None
139 | :keyword true: A symbol interpreted as True.
140 | Default is ``'t'``.
141 | :type false: str or None
142 | :keyword false: A symbol interpreted as False.
143 | Default is ``None``.
144 | :type line_comment: str
145 | :keyword line_comment: Beginning of line comment.
146 | Default is ``';'``.
147 |
148 | >>> loads("(a b)")
149 | [Symbol('a'), Symbol('b')]
150 | >>> loads("a")
151 | Symbol('a')
152 | >>> loads("(a 'b)")
153 | [Symbol('a'), Quoted(Symbol('b'))]
154 | >>> loads("(a '(b))")
155 | [Symbol('a'), Quoted([Symbol('b')])]
156 | >>> loads('''
157 | ... ;; This is a line comment.
158 | ... ("a" "b") ; this is also a comment.
159 | ... ''')
160 | ['a', 'b']
161 | >>> loads('''
162 | ... # This is a line comment.
163 | ... ("a" "b") # this is also a comment.
164 | ... ''', line_comment='#')
165 | ['a', 'b']
166 |
167 | ``nil`` is converted to an empty list by default. You can use
168 | keyword argument `nil` to change what symbol must be interpreted
169 | as nil:
170 |
171 | >>> loads("nil")
172 | []
173 | >>> loads("null", nil='null')
174 | []
175 | >>> loads("nil", nil=None)
176 | Symbol('nil')
177 |
178 | ``t`` is converted to True by default. You can use keyword
179 | argument `true` to change what symbol must be converted to True.:
180 |
181 | >>> loads("t")
182 | True
183 | >>> loads("#t", true='#t')
184 | True
185 | >>> loads("t", true=None)
186 | Symbol('t')
187 |
188 | No symbol is converted to False by default. You can use keyword
189 | argument `false` to convert a symbol to False.
190 |
191 | >>> loads("#f")
192 | Symbol('#f')
193 | >>> loads("#f", false='#f')
194 | False
195 | >>> loads("nil", false='nil', nil=None)
196 | False
197 |
198 | """
199 | obj = parse(string, **kwds)
200 | assert len(obj) == 1 # FIXME: raise an appropriate error
201 | return obj[0]
202 |
203 |
204 | def dump(obj, filelike, **kwds):
205 | """
206 | Write `obj` as an S-expression into given stream `filelike`.
207 |
208 | :arg obj: A Python object.
209 | :arg filelike: A text stream object.
210 |
211 | See :func:`dumps` for valid keyword arguments.
212 |
213 | >>> import io
214 | >>> fp = io.StringIO()
215 | >>> dump(('a', 'b'), fp, str_as='symbol')
216 | >>> print(fp.getvalue())
217 | (a b)
218 |
219 | """
220 | filelike.write(dumps(obj, **kwds))
221 |
222 |
223 | def dumps(obj, **kwds):
224 | """
225 | Convert python object into an S-expression.
226 |
227 | :arg obj: A Python object.
228 | :type str_as: ``'symbol'`` or ``'string'``
229 | :keyword str_as: How string should be interpreted.
230 | Default is ``'string'``.
231 | :type tuple_as: ``'list'`` or ``'array'``
232 | :keyword tuple_as: How tuple should be interpreted.
233 | Default is ``'list'``.
234 | :type true_as: str
235 | :keyword true_as: How True should be interpreted.
236 | Default is ``'t'``
237 | :type false_as: str
238 | :keyword false_as: How False should be interpreted.
239 | Default is ``'()'``
240 | :type none_as: str
241 | :keyword none_as: How None should be interpreted.
242 | Default is ``'()'``
243 | :type pretty_print: bool
244 | :keyword pretty_print: Format output as a tree.
245 | Default is ``False``
246 | :type indent_as: str
247 | :keyword indent_as: String to use for each level of tree indentation.
248 | Default is ``' '``
249 |
250 | Basic usage:
251 |
252 | >>> print(dumps(['a', 'b']))
253 | ("a" "b")
254 | >>> print(dumps(['a', 'b'], str_as='symbol'))
255 | (a b)
256 | >>> print(dumps(dict(a=1)))
257 | (:a 1)
258 | >>> ProperTuple = namedtuple('ProperTuple', 'k')
259 | >>> print(dumps(ProperTuple('v')))
260 | (:k "v")
261 | >>> print(dumps([None, True, False, ()]))
262 | (() t () ())
263 | >>> print(dumps([None, True, False, ()],
264 | ... none_as='null', true_as='#t', false_as='#f'))
265 | (null #t #f ())
266 | >>> print(dumps(('a', 'b')))
267 | ("a" "b")
268 | >>> print(dumps(('a', 'b'), tuple_as='array'))
269 | ["a" "b"]
270 |
271 | More verbose usage:
272 |
273 | >>> print(dumps([Symbol('a'), Symbol('b')]))
274 | (a b)
275 | >>> print(dumps(Symbol('a')))
276 | a
277 | >>> print(dumps([Symbol('a'), Quoted(Symbol('b'))]))
278 | (a 'b)
279 | >>> print(dumps([Symbol('a'), Quoted([Symbol('b')])]))
280 | (a '(b))
281 |
282 | """
283 | return unicode(tosexp(obj, **kwds))
284 |
285 |
286 | def car(obj):
287 | """
288 | Alias of ``obj[0]``.
289 |
290 | >>> car(loads('(a . b)'))
291 | Symbol('a')
292 | >>> car(loads('(a b)'))
293 | Symbol('a')
294 |
295 | """
296 | return obj[0]
297 |
298 |
299 | def cdr(obj):
300 | """
301 | `cdr`-like function.
302 |
303 | >>> cdr(loads('(a . b)'))
304 | Symbol('b')
305 | >>> cdr(loads('(a b)'))
306 | [Symbol('b')]
307 | >>> cdr(loads('(a . (b))'))
308 | [Symbol('b')]
309 | >>> cdr(loads('(a)'))
310 | []
311 | >>> cdr(loads('(a . nil)'))
312 | []
313 |
314 | """
315 | # This is very lazy implementation. Probably the best way to do
316 | # it is to define `Cons` S-expression class.
317 | if len(obj) > 2:
318 | if obj[1] == Symbol('.'):
319 | return obj[2]
320 | return obj[1:]
321 |
322 |
323 | ### Core
324 |
325 | @singledispatch
326 | def tosexp(obj, **kwds):
327 | """
328 | Convert an object to an S-expression (`dumps` is just calling this).
329 |
330 | See this table for comparison of lispy languages, to support them
331 | as much as possible:
332 | `Lisp: Common Lisp, Scheme/Racket, Clojure, Emacs Lisp - Hyperpolyglot
333 | `_
334 |
335 | Most classes can be supported by tosexp() by adding a __to_lisp_as__ method
336 | that returns a restructuring of an instance. The method can use builtin
337 | types, sexpdata hinting classes, and instances of classes that have
338 | tosexp() support.
339 |
340 | Methods that require customizing the recursion or output string of tosexp()
341 | should be registered with @sexpdata.tosexp.register(). Also the default
342 | handlers can be overridden by re-registration.
343 |
344 | Define tosexp() for a simple immutable Cons class. The dot is formatted
345 | rather than doing a 3-tuple w/Symbol('.') hack.
346 |
347 | >>> import sexpdata
348 | >>> class Cons(namedtuple('Cons', 'car cdr')):
349 | ... pass
350 | >>> @sexpdata.tosexp.register(Cons)
351 | ... def _(obj, **kwds):
352 | ... return '({0} . {1})'.format(sexpdata.tosexp(obj.car, **kwds),
353 | ... sexpdata.tosexp(obj.cdr, **kwds))
354 | ...
355 | >>> dumps(Cons(True, False))
356 | '(t . ())'
357 |
358 | A simple alist using Cons:
359 |
360 | >>> dumps(map(Cons, 'abcde', range(5)), str_as='symbol')
361 | '((a . 0) (b . 1) (c . 2) (d . 3) (e . 4))'
362 |
363 | Overriding the float handler for application-wide formatting:
364 |
365 | >>> @sexpdata.tosexp.register(float)
366 | ... def _(obj, **kwds):
367 | ... return '{0:.3}'.format(obj)
368 | ...
369 | >>> import math
370 | >>> tuple(round(math.pi, n) for n in range(5)) # doctest: +SKIP
371 | (3.0, 3.1, 3.14, 3.142, 3.1416)
372 | >>> dumps(round(math.pi, n) for n in range(5))
373 | '(3.0 3.1 3.14 3.14 3.14)'
374 | """
375 | if hasattr(obj, '__to_lisp_as__'):
376 | return tosexp(obj.__to_lisp_as__(), **kwds)
377 | else:
378 | raise TypeError(
379 | "Object of type '{0}' cannot be converted by `tosexp`. "
380 | "It's value is '{1!r}'".format(type(obj), obj))
381 |
382 |
383 | @tosexp.register(Iterable)
384 | @tosexp.register(Mapping)
385 | def _(obj, **kwds):
386 | return tosexp(Parens(obj), **kwds)
387 |
388 |
389 | @tosexp.register(tuple)
390 | def _(obj, tuple_as='list', **kwds):
391 | kwds['tuple_as'] = tuple_as
392 | if hasattr(obj, '__to_lisp_as__'):
393 | return tosexp(obj.__to_lisp_as__(), **kwds)
394 | elif hasattr(obj, '_asdict'):
395 | return tosexp(Parens(obj._asdict()), **kwds)
396 | elif tuple_as == 'list':
397 | return tosexp(Parens(obj), **kwds)
398 | elif tuple_as == 'array':
399 | return tosexp(Brackets(obj), **kwds)
400 | else:
401 | raise ValueError('tuple_as={0!r} is not valid'.format(tuple_as))
402 |
403 |
404 | @tosexp.register(unicode)
405 | def _(obj, str_as='string', **kwds):
406 | kwds['str_as'] = str_as
407 | if str_as == 'symbol':
408 | return obj
409 | elif str_as == 'string':
410 | return tosexp(String(obj))
411 | else:
412 | raise ValueError('str_as={0!r} is not valid'.format(str_as))
413 |
414 |
415 | @tosexp.register(type(None))
416 | def _(obj, none_as='()', **kwds):
417 | return none_as
418 |
419 |
420 | @tosexp.register(bool)
421 | def _(obj, false_as='()', true_as='t', **kwds):
422 | return true_as if obj else false_as
423 |
424 |
425 | @tosexp.register(float)
426 | @tosexp.register(int)
427 | def _(obj, **kwds):
428 | return str(obj)
429 |
430 |
431 | class String(unicode):
432 |
433 | def __eq__(self, other):
434 | """
435 | >>> from itertools import permutations
436 | >>> S = 'a', String('a'), Symbol('a')
437 | >>> all(x == x for x in S)
438 | True
439 | >>> any(x != x for x in S)
440 | False
441 | >>> any(x == y for x, y in permutations(S, 2))
442 | False
443 | >>> all(x != y for x, y in permutations(S, 2))
444 | True
445 | """
446 | return (self.__class__ == other.__class__ and
447 | unicode.__eq__(self, other))
448 |
449 | def __ne__(self, other):
450 | return not self == other
451 |
452 | def __hash__(self):
453 | """
454 | >>> D = {'a': 1, String('a'): 2, Symbol('a'): 3}
455 | >>> len(D)
456 | 3
457 | """
458 | return unicode.__hash__(self)
459 |
460 | _lisp_quoted_specials = [ # from Pymacs
461 | ('\\', '\\\\'), # must come first to avoid doubly quoting "\"
462 | ('"', '\\"'), ('\b', '\\b'), ('\f', '\\f'),
463 | ('\n', '\\n'), ('\r', '\\r'), ('\t', '\\t')]
464 |
465 | _lisp_quoted_to_raw = dict((q, r) for (r, q) in _lisp_quoted_specials)
466 |
467 | def __repr__(self):
468 | return '{0}({1})'.format(self.__class__.__name__,
469 | unicode.__repr__(self))
470 |
471 | @classmethod
472 | def quote(cls, string):
473 | for (s, q) in cls._lisp_quoted_specials:
474 | string = string.replace(s, q)
475 | return string
476 |
477 | @classmethod
478 | def unquote(cls, string):
479 | return cls._lisp_quoted_to_raw.get(string, string)
480 |
481 | def value(self):
482 | return unicode(self)
483 |
484 |
485 | @tosexp.register(String)
486 | def _(obj, **kwds):
487 | return '"' + String.quote(obj) + '"'
488 |
489 |
490 | class Symbol(String):
491 |
492 | _lisp_quoted_specials = [
493 | ('\\', '\\\\'), # must come first to avoid doubly quoting "\"
494 | ("'", r"\'"), ("`", r"\`"), ('"', r'\"'),
495 | ('(', r'\('), (')', r'\)'), ('[', r'\['), (']', r'\]'),
496 | (' ', r'\ '), (',', r'\,'), ('?', r'\?'),
497 | (';', r'\;'), ('#', r'\#'),
498 | ]
499 |
500 | _lisp_quoted_to_raw = dict((q, r) for (r, q) in _lisp_quoted_specials)
501 |
502 |
503 | @tosexp.register(Symbol)
504 | def _(obj, **kwds):
505 | return Symbol.quote(obj)
506 |
507 |
508 | class Quoted(namedtuple('Quoted', 'x')):
509 |
510 | def __repr__(self):
511 | return '{0.__class__.__name__}({0.x!r})'.format(self)
512 |
513 | @tosexp.register(Quoted)
514 | def _(obj, **kwds):
515 | return "'" + tosexp(obj.x, **kwds)
516 |
517 |
518 | class Delimiters(namedtuple('Delimiters', 'I')):
519 |
520 | def __new__(cls, *args):
521 | if not args:
522 | raise ValueError("Expected an Iterable/Mapping argument or *args")
523 | x = args[0] if len(args) == 1 else args
524 |
525 | if isinstance(x, Mapping):
526 | plist_pairs = ((Symbol(':' + k), v) for k, v in x.items())
527 | return tuple.__new__(cls, (tuple(chain.from_iterable(plist_pairs)),))
528 | elif isinstance(x, (unicode, bytes)) or not isinstance(x, Iterable):
529 | return tuple.__new__(cls, ((x,),)) # unary *args
530 | elif isinstance(x, Sequence):
531 | return tuple.__new__(cls, (x,))
532 | else: # isinstance(x, Iterable)
533 | return tuple.__new__(cls, (tuple(x),))
534 |
535 | @staticmethod
536 | def from_opener(opener, val):
537 | cls_map = dict((cls.opener, cls) for cls in Delimiters.__subclasses__())
538 | if opener in cls_map.keys():
539 | return cls_map[opener](val)
540 | else:
541 | raise TypeError
542 |
543 | @staticmethod
544 | def get_brackets():
545 | return {cls.opener: cls.closer for cls in Delimiters.__subclasses__()}
546 |
547 | @tosexp.register(Delimiters)
548 | def _(self, **kwds):
549 | # Don't break up expressions produced by certain overloads of tosexp
550 | dont_break = all(tosexp.dispatch(type(x)) not in DONT_BREAK_OVERLOADS for x in self.I)
551 |
552 | if "pretty_print" in kwds and kwds["pretty_print"] and not dont_break:
553 | expr_separator = "\n"
554 | exprs_indent = kwds["indent_as"] if "indent_as" in kwds else " "
555 | exprs_separator = "\n"
556 | else:
557 | expr_separator = " "
558 | exprs_indent = ""
559 | exprs_separator = ""
560 |
561 | exprs = expr_separator.join(tosexp(x, **kwds) for x in self.I)
562 | indented_exprs = "".join(exprs_indent + line for line in exprs.splitlines(True))
563 |
564 | return (self.__class__.opener +
565 | exprs_separator +
566 | indented_exprs +
567 | exprs_separator +
568 | self.__class__.closer)
569 | DONT_BREAK_OVERLOADS = [tosexp.dispatch(c) for c in (object, Iterable, Mapping, tuple, Delimiters)]
570 |
571 |
572 | class Brackets(Delimiters):
573 | """
574 | Outputs an Iterable or Mapping with square brackets.
575 |
576 | Selectively make a container an array:
577 |
578 | >>> dumps(Brackets(list(range(5))))
579 | '[0 1 2 3 4]'
580 |
581 | >>> dumps(Brackets(dict(a=1)))
582 | '[:a 1]'
583 | """
584 |
585 | opener, closer = '[', ']'
586 |
587 |
588 | class Parens(Delimiters):
589 | """
590 | Outputs an Iterable or Mapping with parentheses.
591 |
592 | By default Iterables and Mappings output with parentheses.
593 |
594 | >>> dumps(range(5))
595 | '(0 1 2 3 4)'
596 | >>> dumps(dict(a=1))
597 | '(:a 1)'
598 |
599 | Selectively override the tuple_as='array' default parameter:
600 |
601 | >>> dumps((0, Parens((1, 2, 3)), 4), tuple_as='array')
602 | '[0 (1 2 3) 4]'
603 | """
604 |
605 | opener, closer = '(', ')'
606 |
607 |
608 | def bracket(val, bra):
609 | if bra == '(':
610 | return val
611 | else:
612 | return Delimiters.from_opener(bra, val)
613 |
614 |
615 | class ExpectClosingBracket(Exception):
616 |
617 | def __init__(self, got, expect):
618 | super(ExpectClosingBracket, self).__init__(
619 | "Not enough closing brackets. "
620 | "Expected {0!r} to be the last letter in the sexp. "
621 | "Got: {1!r}".format(expect, got))
622 |
623 |
624 | class ExpectNothing(Exception):
625 |
626 | def __init__(self, got):
627 | super(ExpectNothing, self).__init__(
628 | "Too many closing brackets. "
629 | "Expected no character left in the sexp. "
630 | "Got: {0!r}".format(got))
631 |
632 | class ExpectSExp(Exception):
633 |
634 | def __init__(self, pos):
635 | super(ExpectSExp, self).__init__(
636 | 'No s-exp is found after an apostrophe'
637 | ' at position {0}'.format(pos))
638 |
639 |
640 | class Parser(object):
641 |
642 | brackets: dict
643 | closing_brackets: set
644 | _atom_end_basic: set
645 | _atom_end_basic_or_escape_regexp: str
646 |
647 |
648 | def __init__(self, string, string_to=None, nil='nil', true='t', false=None,
649 | line_comment=';'):
650 | self.string = string
651 | self.nil = nil
652 | self.true = true
653 | self.false = false
654 | self.string_to = (lambda x: x) if string_to is None else string_to
655 | self.line_comment = line_comment
656 |
657 | # Compute brackets from delimiter
658 | self.brackets = Delimiters.get_brackets()
659 | self.closing_brackets = set(self.brackets.values())
660 | self._atom_end_basic = \
661 | set(self.brackets) | set(self.closing_brackets) | \
662 | set('"') | set(whitespace)
663 | self._atom_end_basic_or_escape_regexp = "|".join(map(re.escape,
664 | self._atom_end_basic | set('\\')))
665 | self.quote_or_escape_re = re.compile(r'"|\\')
666 | self.atom_end = set([line_comment]) | self._atom_end_basic
667 | self.atom_end_or_escape_re = \
668 | re.compile("{0}|{1}".format(self._atom_end_basic_or_escape_regexp,
669 | re.escape(line_comment)))
670 |
671 |
672 | def parse_str(self, i):
673 | string = self.string
674 | chars = []
675 | append = chars.append
676 | search = self.quote_or_escape_re.search
677 |
678 | assert string[i] == '"' # never fail
679 | while True:
680 | i += 1
681 | match = search(string, i)
682 | end = match.start()
683 | append(string[i:end])
684 | c = match.group()
685 | if c == '"':
686 | i = end + 1
687 | break
688 | elif c == '\\':
689 | i = end + 1
690 | append(String.unquote(c + string[i]))
691 | else:
692 | raise ExpectClosingBracket('"', None)
693 | return (i, ''.join(chars))
694 |
695 | def parse_atom(self, i):
696 | string = self.string
697 | chars = []
698 | append = chars.append
699 | search = self.atom_end_or_escape_re.search
700 | atom_end = self.atom_end
701 |
702 | while True:
703 | match = search(string, i)
704 | if not match:
705 | append(string[i:])
706 | i = len(string)
707 | break
708 | end = match.start()
709 | append(string[i:end])
710 | c = match.group()
711 | if c in atom_end:
712 | i = end # this is different from str
713 | break
714 | elif c == '\\':
715 | i = end + 1
716 | append(Symbol.unquote(c + string[i]))
717 | i += 1
718 | else:
719 | raise ExpectClosingBracket('"', None)
720 | return (i, self.atom(''.join(chars)))
721 |
722 | def atom(self, token):
723 | if token == self.nil:
724 | return []
725 | if token == self.true:
726 | return True
727 | if token == self.false:
728 | return False
729 | try:
730 | return int(token)
731 | except ValueError:
732 | try:
733 | return float(token)
734 | except ValueError:
735 | return Symbol(token)
736 |
737 | def parse_sexp(self, i):
738 | string = self.string
739 | len_string = len(self.string)
740 | sexp = []
741 | append = sexp.append
742 | while i < len_string:
743 | c = string[i]
744 | if c == '"':
745 | (i, subsexp) = self.parse_str(i)
746 | append(self.string_to(subsexp))
747 | elif c in whitespace:
748 | i += 1
749 | continue
750 | elif c in self.brackets:
751 | close = self.brackets[c]
752 | (i, subsexp) = self.parse_sexp(i + 1)
753 | append(bracket(subsexp, c))
754 | try:
755 | nc = string[i]
756 | except IndexError:
757 | nc = None
758 | if nc != close:
759 | raise ExpectClosingBracket(nc, close)
760 | i += 1
761 | elif c in self.closing_brackets:
762 | break
763 | elif c == "'":
764 | next_parse_start = i + 1
765 | (i, subsexp) = self.parse_sexp(next_parse_start)
766 | if not subsexp:
767 | raise ExpectSExp(next_parse_start - 1)
768 | append(Quoted(subsexp[0]))
769 | sexp.extend(subsexp[1:])
770 | elif c == self.line_comment:
771 | i = string.find('\n', i) + 1
772 | if i <= 0:
773 | i = len_string
774 | break
775 | else:
776 | (i, subsexp) = self.parse_atom(i)
777 | append(subsexp)
778 | return (i, sexp)
779 |
780 | def parse(self):
781 | (i, sexp) = self.parse_sexp(0)
782 | if i < len(self.string):
783 | raise ExpectNothing(self.string[i:])
784 | return sexp
785 |
786 |
787 | def parse(string, **kwds):
788 | """
789 | Parse s-expression.
790 |
791 | >>> parse("(a b)")
792 | [[Symbol('a'), Symbol('b')]]
793 | >>> parse("a")
794 | [Symbol('a')]
795 | >>> parse("(a 'b)")
796 | [[Symbol('a'), Quoted(Symbol('b'))]]
797 | >>> parse("(a '(b))")
798 | [[Symbol('a'), Quoted([Symbol('b')])]]
799 |
800 | """
801 | assert type(string)==str
802 | return Parser(string, **kwds).parse()
803 |
--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steffen-W/KiCad-Parasitics/e2dc5647b86c2316b10a04626640105d2dc302b4/resources/icon.png
--------------------------------------------------------------------------------