├── .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 | ![grafik](https://user-images.githubusercontent.com/3403218/274055069-4780a4f3-2c2f-4d14-8325-577f7d687760.png) 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 | - ![grafik](https://user-images.githubusercontent.com/3403218/274056663-e2c870e7-c23e-4c59-855d-cbc0a39c98f6.png) 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 --------------------------------------------------------------------------------