diff --git a/.gitignore b/.gitignore index 1401c0ad..aa531c3d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,11 +11,8 @@ gurobi.log /benchmarks /logs /notebooks -/data/timezone_mappings.csv -/data/urban_percent.csv /data/links_p_nom.csv /data/*totals.csv -/data/*Jensen.csv /data/biomass* /data/emobility/ /data/eea* @@ -28,6 +25,7 @@ gurobi.log /data/.nfs* /data/Industrial_Database.csv /data/retro/tabula-calculator-calcsetbuilding.csv +/data/nuts* *.org diff --git a/.syncignore-receive b/.syncignore-receive new file mode 100644 index 00000000..3ebcbea8 --- /dev/null +++ b/.syncignore-receive @@ -0,0 +1,14 @@ +.snakemake +.git +.pytest_cache +.ipynb_checkpoints +.vscode +.DS_Store +__pycache__ +*.pyc +*.pyo +*.ipynb +data +notebooks +benchmarks +*.nc \ No newline at end of file diff --git a/.syncignore-send b/.syncignore-send new file mode 100644 index 00000000..38f4b664 --- /dev/null +++ b/.syncignore-send @@ -0,0 +1,14 @@ +.snakemake +.git +.pytest_cache +.ipynb_checkpoints +.vscode +.DS_Store +__pycache__ +*.pyc +*.pyo +*.ipynb +notebooks +benchmarks +resources +results \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index 9cecc1d4..dc10fd32 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,674 +1,20 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright 2017-2021 The PyPSA-Eur Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index fe81519b..b6929873 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,24 @@ -**WARNING**: This model is under construction and contains serious -problems that distort the results. See the github repository -[issues](https://github.com/PyPSA/pypsa-eur-sec/issues) for some of -the problems (please feel free to help or make suggestions). There is -neither documentation nor a paper yet, but we hope to have a preprint -out by autumn 2021. We cannot support this model if you choose to use -it. +**WARNING**: This model is under construction and contains serious problems that +distort the results. See the github repository +[issues](https://github.com/PyPSA/pypsa-eur-sec/issues) for some of the problems +(please feel free to help or make suggestions). There is neither a full +documentation nor a paper yet, but we hope to have a preprint out by the end of 2021. +You can find out more about the model capabilities in [a recent +presentation at EMP-E](https://nworbmot.org/energy/brown-empe.pdf) or the +following [preprint with a description of the industry +sector](https://arxiv.org/abs/2109.09563). We cannot support this model if you +choose to use it. PyPSA-Eur-Sec builds on the electricity generation and transmission model [PyPSA-Eur](https://github.com/PyPSA/pypsa-eur) to add demand and supply for the following sectors: transport, space and water -heating, biomass, industry and industrial feedstocks. This completes -the energy system and includes all greenhouse gas emitters except -waste management, agriculture, forestry and land use. +heating, biomass, industry and industrial feedstocks, agriculture, +forestry and fishing. This completes the energy system and includes +all greenhouse gas emitters except waste management and land use. Please see the [documentation](https://pypsa-eur-sec.readthedocs.io/) for installation instructions and other useful information about the snakemake workflow. @@ -65,6 +68,6 @@ the additional sectors. # Licence The code in PyPSA-Eur-Sec is released as free software under the -[GPLv3](http://www.gnu.org/licenses/gpl-3.0.en.html), see LICENSE.txt. +[MIT License](https://opensource.org/licenses/MIT), see `LICENSE.txt`. However, different licenses and terms of use may apply to the various input data. diff --git a/Snakefile b/Snakefile index 77b9238b..1bec5683 100644 --- a/Snakefile +++ b/Snakefile @@ -1,4 +1,7 @@ +from snakemake.remote.HTTP import RemoteProvider as HTTPRemoteProvider +HTTP = HTTPRemoteProvider() + configfile: "config.yaml" @@ -6,7 +9,6 @@ wildcard_constraints: lv="[a-z0-9\.]+", simpl="[a-zA-Z0-9]*", clusters="[0-9]+m?", - sectors="[+a-zA-Z0-9]+", opts="[-+a-zA-Z0-9]*", sector_opts="[-+a-zA-Z0-9\.\s]*" @@ -21,7 +23,6 @@ subworkflow pypsaeur: snakefile: "../pypsa-eur/Snakefile" configfile: "../pypsa-eur/config.yaml" - rule all: input: SDIR + '/graphs/costs.pdf' @@ -167,6 +168,7 @@ rule build_energy_totals: co2="data/eea/UNFCCC_v23.csv", swiss="data/switzerland-sfoe/switzerland-new_format.csv", idees="data/jrc-idees-2015", + district_heat_share='data/district_heat_share.csv', eurostat=input_eurostat output: energy_name='resources/energy_totals.csv', @@ -180,16 +182,37 @@ rule build_energy_totals: rule build_biomass_potentials: input: - jrc_potentials="data/biomass/JRC Biomass Potentials.xlsx" + enspreso_biomass=HTTP.remote("https://cidportal.jrc.ec.europa.eu/ftp/jrc-opendata/ENSPRESO/ENSPRESO_BIOMASS.xlsx", keep_local=True), + nuts2="data/nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson", # https://gisco-services.ec.europa.eu/distribution/v2/nuts/download/#nuts21 + regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), + nuts3_population="../pypsa-eur/data/bundle/nama_10r_3popgdp.tsv.gz", + swiss_cantons="../pypsa-eur/data/bundle/ch_cantons.csv", + swiss_population="../pypsa-eur/data/bundle/je-e-21.03.02.xls", + country_shapes=pypsaeur('resources/country_shapes.geojson') output: - biomass_potentials_all='resources/biomass_potentials_all.csv', - biomass_potentials='resources/biomass_potentials.csv' + biomass_potentials_all='resources/biomass_potentials_all_s{simpl}_{clusters}.csv', + biomass_potentials='resources/biomass_potentials_s{simpl}_{clusters}.csv' threads: 1 resources: mem_mb=1000 - benchmark: "benchmarks/build_biomass_potentials" + benchmark: "benchmarks/build_biomass_potentials_s{simpl}_{clusters}" script: 'scripts/build_biomass_potentials.py' +if config["sector"]["biomass_transport"]: + rule build_biomass_transport_costs: + input: + transport_cost_data=HTTP.remote("publications.jrc.ec.europa.eu/repository/bitstream/JRC98626/biomass potentials in europe_web rev.pdf", keep_local=True) + output: + biomass_transport_costs="resources/biomass_transport_costs.csv", + threads: 1 + resources: mem_mb=1000 + benchmark: "benchmarks/build_biomass_transport_costs" + script: 'scripts/build_biomass_transport_costs.py' + build_biomass_transport_costs_output = rules.build_biomass_transport_costs.output +else: + build_biomass_transport_costs_output = {} + + rule build_ammonia_production: input: usgs="data/myb1-2017-nitro.xls" @@ -230,10 +253,10 @@ rule build_industrial_production_per_country_tomorrow: input: industrial_production_per_country="resources/industrial_production_per_country.csv" output: - industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow.csv" + industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow_{planning_horizons}.csv" threads: 1 resources: mem_mb=1000 - benchmark: "benchmarks/build_industrial_production_per_country_tomorrow" + benchmark: "benchmarks/build_industrial_production_per_country_tomorrow_{planning_horizons}" script: 'scripts/build_industrial_production_per_country_tomorrow.py' @@ -253,25 +276,25 @@ rule build_industrial_distribution_key: rule build_industrial_production_per_node: input: industrial_distribution_key="resources/industrial_distribution_key_elec_s{simpl}_{clusters}.csv", - industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow.csv" + industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow_{planning_horizons}.csv" output: - industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}.csv" + industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv" threads: 1 resources: mem_mb=1000 - benchmark: "benchmarks/build_industrial_production_per_node/s{simpl}_{clusters}" + benchmark: "benchmarks/build_industrial_production_per_node/s{simpl}_{clusters}_{planning_horizons}" script: 'scripts/build_industrial_production_per_node.py' rule build_industrial_energy_demand_per_node: input: industry_sector_ratios="resources/industry_sector_ratios.csv", - industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}.csv", + industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv", industrial_energy_demand_per_node_today="resources/industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv" output: - industrial_energy_demand_per_node="resources/industrial_energy_demand_elec_s{simpl}_{clusters}.csv" + industrial_energy_demand_per_node="resources/industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" threads: 1 resources: mem_mb=1000 - benchmark: "benchmarks/build_industrial_energy_demand_per_node/s{simpl}_{clusters}" + benchmark: "benchmarks/build_industrial_energy_demand_per_node/s{simpl}_{clusters}_{planning_horizons}" script: 'scripts/build_industrial_energy_demand_per_node.py' @@ -334,7 +357,7 @@ rule prepare_sector_network: clustered_gas_network="resources/gas_network_elec_s{simpl}_{clusters}.csv", traffic_data_KFZ="data/emobility/KFZ__count", traffic_data_Pkw="data/emobility/Pkw__count", - biomass_potentials='resources/biomass_potentials.csv', + biomass_potentials='resources/biomass_potentials_s{simpl}_{clusters}.csv', heat_profile="data/heat_load_profile_BDEW.csv", costs=CDIR + "costs_{planning_horizons}.csv", profile_offwind_ac=pypsaeur("resources/profile_offwind-ac.nc"), @@ -344,7 +367,7 @@ rule prepare_sector_network: busmap=pypsaeur("resources/busmap_elec_s{simpl}_{clusters}.csv"), clustered_pop_layout="resources/pop_layout_elec_s{simpl}_{clusters}.csv", simplified_pop_layout="resources/pop_layout_elec_s{simpl}.csv", - industrial_demand="resources/industrial_energy_demand_elec_s{simpl}_{clusters}.csv", + industrial_demand="resources/industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", heat_demand_urban="resources/heat_demand_urban_elec_s{simpl}_{clusters}.nc", heat_demand_rural="resources/heat_demand_rural_elec_s{simpl}_{clusters}.nc", heat_demand_total="resources/heat_demand_total_elec_s{simpl}_{clusters}.nc", @@ -363,7 +386,8 @@ rule prepare_sector_network: solar_thermal_total="resources/solar_thermal_total_elec_s{simpl}_{clusters}.nc", solar_thermal_urban="resources/solar_thermal_urban_elec_s{simpl}_{clusters}.nc", solar_thermal_rural="resources/solar_thermal_rural_elec_s{simpl}_{clusters}.nc", - **build_retro_cost_output + **build_retro_cost_output, + **build_biomass_transport_costs_output output: RDIR + '/prenetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc' threads: 1 resources: mem_mb=2000 diff --git a/config.default.yaml b/config.default.yaml index 5c9e4958..1254e926 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -1,4 +1,4 @@ -version: 0.5.0 +version: 0.6.0 logging_level: INFO @@ -21,15 +21,17 @@ scenario: opts: # only relevant for PyPSA-Eur - '' sector_opts: # this is where the main scenario settings are - - Co2L0-3H-T-H-B-I-solar+p3-dist1 + - Co2L0-3H-T-H-B-I-A-solar+p3-dist1 # to really understand the options here, look in scripts/prepare_sector_network.py # Co2Lx specifies the CO2 target in x% of the 1990 values; default will give default (5%); # Co2L0p25 will give 25% CO2 emissions; Co2Lm0p05 will give 5% negative emissions # xH is the temporal resolution; 3H is 3-hourly, i.e. one snapshot every 3 hours # single letters are sectors: T for land transport, H for building heating, - # B for biomass supply, I for industry, shipping and aviation + # B for biomass supply, I for industry, shipping and aviation, + # A for agriculture, forestry and fishing # solar+c0.5 reduces the capital cost of solar to 50\% of reference value # solar+p3 multiplies the available installable potential by factor 3 + # co2 stored+e2 multiplies the potential of CO2 sequestration by a factor 2 # dist{n} includes distribution grids with investment cost of n times cost in data/costs.csv # for myopic/perfect foresight cb states the carbon budget in GtCO2 (cumulative # emissions throughout the transition path in the timeframe determined by the @@ -71,7 +73,8 @@ electricity: # regulate what components with which carriers are kept from PyPSA-Eur; # some technologies are removed because they are implemented differently -# or have different year-dependent costs in PyPSA-Eur-Sec +# (e.g. battery or H2 storage) or have different year-dependent costs +# in PyPSA-Eur-Sec pypsa_eur: Bus: - AC @@ -97,28 +100,28 @@ energy: biomass: year: 2030 - scenario: Med + scenario: ENS_Med classes: solid biomass: - - Primary agricultural residues - - Forestry energy residue - - Secondary forestry residues - - Secondary Forestry residues sawdust - - Forestry residues from landscape care biomass + - Agricultural waste + - Fuelwood residues + - Secondary Forestry residues - woodchips + - Sawdust + - Residues from landscape care - Municipal waste not included: - - Bioethanol sugar beet biomass - - Rapeseeds for biodiesel - - sunflower and soya for Biodiesel - - Starchy crops biomass - - Grassy crops biomass - - Willow biomass - - Poplar biomass potential - - Roundwood fuelwood - - Roundwood Chips & Pellets + - Sugar from sugar beet + - Rape seed + - "Sunflower, soya seed " + - Bioethanol barley, wheat, grain maize, oats, other cereals and rye + - Miscanthus, switchgrass, RCG + - Willow + - Poplar + - FuelwoodRW + - C&P_RW biogas: - - Manure biomass potential - - Sludge biomass + - Manure solid, liquid + - Sludge solar_thermal: @@ -139,8 +142,16 @@ existing_capacities: sector: - central: true - central_fraction: 0.6 + district_heating: + potential: 0.6 # maximum fraction of urban demand which can be supplied by district heating + # increase of today's district heating demand to potential maximum district heating share + # progress = 0 means today's district heating share, progress = 1 means maximum fraction of urban demand is supplied by district heating + progress: 1 + # 2020: 0.0 + # 2030: 0.3 + # 2040: 0.6 + # 2050: 1.0 + district_heating_loss: 0.15 bev_dsm_restriction_value: 0.75 #Set to 0 for no restriction on BEV DSM bev_dsm_restriction_time: 7 #Time at which SOC of BEV has to be dsm_restriction_value transport_heating_deadband_upper: 20. @@ -149,7 +160,6 @@ sector: ICE_upper_degree_factor: 1.6 EV_lower_degree_factor: 0.98 EV_upper_degree_factor: 0.63 - district_heating_loss: 0.15 bev_dsm: true #turns on EV battery bev_availability: 0.5 #How many cars do smart charging bev_energy: 0.05 #average battery size in MWh @@ -160,34 +170,46 @@ sector: bev_avail_mean: 0.8 v2g: true #allows feed-in to grid from EV battery #what is not EV or FCEV is oil-fuelled ICE - land_transport_fuel_cell_share: # 1 means all FCEVs - 2020: 0 - 2030: 0.05 - 2040: 0.1 - 2050: 0.15 - land_transport_electric_share: # 1 means all EVs - 2020: 0 - 2030: 0.25 - 2040: 0.6 - 2050: 0.85 + land_transport_fuel_cell_share: 0.15 # 1 means all FCEVs + # 2020: 0 + # 2030: 0.05 + # 2040: 0.1 + # 2050: 0.15 + land_transport_electric_share: 0.85 # 1 means all EVs + # 2020: 0 + # 2030: 0.25 + # 2040: 0.6 + # 2050: 0.85 transport_fuel_cell_efficiency: 0.5 transport_internal_combustion_efficiency: 0.3 + agriculture_machinery_electric_share: 0 + agriculture_machinery_fuel_efficiency: 0.7 # fuel oil per use + agriculture_machinery_electric_efficiency: 0.3 # electricity per use shipping_average_efficiency: 0.4 #For conversion of fuel oil to propulsion in 2011 + shipping_hydrogen_liquefaction: false # whether to consider liquefaction costs for shipping H2 demands + shipping_hydrogen_share: 1 # 1 means all hydrogen FC + # 2020: 0 + # 2025: 0 + # 2030: 0.05 + # 2035: 0.15 + # 2040: 0.3 + # 2045: 0.6 + # 2050: 1 time_dep_hp_cop: true #time dependent heat pump coefficient of performance heat_pump_sink_T: 55. # Celsius, based on DTU / large area radiators; used in build_cop_profiles.py # conservatively high to cover hot water and space heating in poorly-insulated buildings reduce_space_heat_exogenously: true # reduces space heat demand by a given factor (applied before losses in DH) # this can represent e.g. building renovation, building demolition, or if # the factor is negative: increasing floor area, increased thermal comfort, population growth - reduce_space_heat_exogenously_factor: # per unit reduction in space heat demand + reduce_space_heat_exogenously_factor: 0.29 # per unit reduction in space heat demand # the default factors are determined by the LTS scenario from http://tool.european-calculator.eu/app/buildings/building-types-area/?levers=1ddd4444421213bdbbbddd44444ffffff11f411111221111211l212221 - 2020: 0.10 # this results in a space heat demand reduction of 10% - 2025: 0.09 # first heat demand increases compared to 2020 because of larger floor area per capita - 2030: 0.09 - 2035: 0.11 - 2040: 0.16 - 2045: 0.21 - 2050: 0.29 + # 2020: 0.10 # this results in a space heat demand reduction of 10% + # 2025: 0.09 # first heat demand increases compared to 2020 because of larger floor area per capita + # 2030: 0.09 + # 2035: 0.11 + # 2040: 0.16 + # 2045: 0.21 + # 2050: 0.29 retrofitting : # co-optimises building renovation to reduce space heat demand retro_endogen: false # co-optimise space heat savings cost_factor: 1.0 # weight costs for building renovation @@ -212,7 +234,8 @@ sector: co2_vent: true SMR: true co2_sequestration_potential: 200 #MtCO2/a sequestration potential for Europe - co2_sequestration_cost: 20 #EUR/tCO2 for transport and sequestration of CO2 + co2_sequestration_cost: 10 #EUR/tCO2 for sequestration of CO2 + co2_network: false cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture hydrogen_underground_storage: true use_fischer_tropsch_waste_heat: true @@ -228,25 +251,61 @@ sector: H2_retrofit_capacity_per_CH4: 0.6 # ratio for H2 capacity per original CH4 capacity of retrofitted pipelines gas_distribution_grid: true gas_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv + biomass_transport: false # biomass transport between nodes conventional_generation: # generator : carrier OCGT: gas industry: - St_primary_fraction: 0.3 # fraction of steel produced via primary route (DRI + EAF) versus secondary route (EAF); today fraction is 0.6 + St_primary_fraction: 0.3 # fraction of steel produced via primary route versus secondary route (scrap+EAF); today fraction is 0.6 + # 2020: 0.6 + # 2025: 0.55 + # 2030: 0.5 + # 2035: 0.45 + # 2040: 0.4 + # 2045: 0.35 + # 2050: 0.3 + DRI_fraction: 1 # fraction of the primary route converted to DRI + EAF + # 2020: 0 + # 2025: 0 + # 2030: 0.05 + # 2035: 0.2 + # 2040: 0.4 + # 2045: 0.7 + # 2050: 1 H2_DRI: 1.7 #H2 consumption in Direct Reduced Iron (DRI), MWh_H2,LHV/ton_Steel from 51kgH2/tSt in Vogl et al (2018) doi:10.1016/j.jclepro.2018.08.279 elec_DRI: 0.322 #electricity consumption in Direct Reduced Iron (DRI) shaft, MWh/tSt HYBRIT brochure https://ssabwebsitecdn.azureedge.net/-/media/hybrit/files/hybrit_brochure.pdf Al_primary_fraction: 0.2 # fraction of aluminium produced via the primary route versus scrap; today fraction is 0.4 + # 2020: 0.4 + # 2025: 0.375 + # 2030: 0.35 + # 2035: 0.325 + # 2040: 0.3 + # 2045: 0.25 + # 2050: 0.2 MWh_CH4_per_tNH3_SMR: 10.8 # 2012's demand from https://ec.europa.eu/docsroom/documents/4165/attachments/1/translations/en/renditions/pdf MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3 MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy) MWh_elec_per_tNH3_electrolysis: 1.17 # from https://doi.org/10.1016/j.joule.2018.04.017 Table 13 (air separation and HB) NH3_process_emissions: 24.5 # in MtCO2/a from SMR for H2 production for NH3 from UNFCCC for 2015 for EU28 petrochemical_process_emissions: 25.5 # in MtCO2/a for petrochemical and other from UNFCCC for 2015 for EU28 - HVC_primary_fraction: 1.0 #fraction of current non-ammonia basic chemicals produced via primary route + HVC_primary_fraction: 1. # fraction of today's HVC produced via primary route + HVC_mechanical_recycling_fraction: 0. # fraction of today's HVC produced via mechanical recycling + HVC_chemical_recycling_fraction: 0. # fraction of today's HVC produced via chemical recycling + HVC_production_today: 52. # MtHVC/a from DECHEMA (2017), Figure 16, page 107; includes ethylene, propylene and BTX + MWh_elec_per_tHVC_mechanical_recycling: 0.547 # from SI of https://doi.org/10.1016/j.resconrec.2020.105010, Table S5, for HDPE, PP, PS, PET. LDPE would be 0.756. + MWh_elec_per_tHVC_chemical_recycling: 6.9 # Material Economics (2019), page 125; based on pyrolysis and electric steam cracking + chlorine_production_today: 9.58 # MtCl/a from DECHEMA (2017), Table 7, page 43 + MWh_elec_per_tCl: 3.6 # DECHEMA (2017), Table 6, page 43 + MWh_H2_per_tCl: -0.9372 # DECHEMA (2017), page 43; negative since hydrogen produced in chloralkali process + methanol_production_today: 1.5 # MtMeOH/a from DECHEMA (2017), page 62 + MWh_elec_per_tMeOH: 0.167 # DECHEMA (2017), Table 14, page 65 + MWh_CH4_per_tMeOH: 10.25 # DECHEMA (2017), Table 14, page 65 hotmaps_locate_missing: false reference_year: 2015 - + # references: + # DECHEMA (2017): https://dechema.de/dechema_media/Downloads/Positionspapiere/Technology_study_Low_carbon_energy_and_feedstock_for_the_European_chemical_industry-p-20002750.pdf + # Material Economics (2019): https://materialeconomics.com/latest-updates/industrial-transformation-2050 costs: lifetime: 25 #default lifetime @@ -308,7 +367,7 @@ solving: plotting: map: - boundaries: [-11, 30, 34, 71] + boundaries: [-11, 30, 34, 71] color_geomap: ocean: white land: whitesmoke @@ -360,6 +419,7 @@ plotting: - solar thermal collector - central solar thermal collector tech_colors: + # wind onwind: "#235ebc" onshore wind: "#235ebc" offwind: "#6895dd" @@ -368,117 +428,161 @@ plotting: offshore wind (AC): "#6895dd" offwind-dc: "#74c6f2" offshore wind (DC): "#74c6f2" - wave: '#004444' - hydro: '#3B5323' - hydro reservoir: '#3B5323' - ror: '#78AB46' - run of river: '#78AB46' - hydroelectricity: '#006400' + # water + hydro: '#298c81' + hydro reservoir: '#298c81' + ror: '#3dbfb0' + run of river: '#3dbfb0' + hydroelectricity: '#298c81' + PHS: '#51dbcc' + wave: '#a7d4cf' + # solar solar: "#f9d002" solar PV: "#f9d002" - solar thermal: coral - solar rooftop: '#ffef60' - OCGT: wheat - OCGT marginal: sandybrown - OCGT-heat: '#ee8340' - gas boiler: '#ee8340' - gas boilers: '#ee8340' - gas boiler marginal: '#ee8340' - gas-to-power/heat: '#ee8340' - gas: brown - Gas pipeline : brown - natural gas: brown - SMR: '#4F4F2F' - SMR CC: '#6f6f42' - oil: '#B5A642' - oil boiler: '#B5A677' - lines: k - transmission lines: k - H2: m - hydrogen storage: m - battery: slategray - battery storage: slategray - home battery: '#614700' - home battery storage: '#614700' - Nuclear: r - Nuclear marginal: r - nuclear: r - uranium: r - Coal: k - coal: k - Coal marginal: k - Lignite: grey - lignite: grey - Lignite marginal: grey - CCGT: '#ee8340' - CCGT marginal: '#ee8340' - heat pumps: '#76EE00' - heat pump: '#76EE00' - air heat pump: '#76EE00' - ground heat pump: '#40AA00' - power-to-heat: '#40AA00' - resistive heater: pink - Sabatier: '#FF1493' - methanation: '#FF1493' - power-to-gas: '#FF1493' - power-to-liquid: '#FFAAE9' - helmeth: '#7D0552' - DAC: '#E74C3C' - co2 stored: '#123456' - CO2 sequestration: '#123456' - CC: k - co2: '#123456' - co2 vent: '#654321' - solid biomass for industry co2 from atmosphere: '#654321' - solid biomass for industry co2 to stored: '#654321' - gas for industry co2 to atmosphere: '#654321' - gas for industry co2 to stored: '#654321' - Fischer-Tropsch: '#44DD33' - kerosene for aviation: '#44BB11' - naphtha for industry: '#44FF55' - land transport oil: '#44DD33' - water tanks: '#BBBBBB' - hot water storage: '#BBBBBB' - hot water charging: '#BBBBBB' - hot water discharging: '#999999' - CHP: r - CHP heat: r - CHP electric: r - PHS: g - Ambient: k - Electric load: b - Heat load: r - heat: darkred - rural heat: '#880000' - central heat: '#b22222' - decentral heat: '#800000' - low-temperature heat for industry: '#991111' - process heat: '#FF3333' - heat demand: darkred - electric demand: k - Li ion: grey - district heating: '#CC4E5C' - retrofitting: purple - building retrofitting: purple - BEV charger: grey - V2G: grey - land transport EV: grey - electricity: k - gas for industry: '#333333' - gas for industry CC: '#404040' - solid biomass for industry: '#555555' - solid biomass for industry CC: '#555555' - industry electricity: '#222222' - industry new electricity: '#222222' + solar thermal: '#ffbf2b' + solar rooftop: '#ffea80' + # gas + OCGT: '#e0986c' + OCGT marginal: '#e0986c' + OCGT-heat: '#e0986c' + gas boiler: '#db6a25' + gas boilers: '#db6a25' + gas boiler marginal: '#db6a25' + gas: '#e05b09' + natural gas: '#e05b09' + CCGT: '#a85522' + CCGT marginal: '#a85522' + gas for industry co2 to atmosphere: '#692e0a' + gas for industry co2 to stored: '#8a3400' + gas for industry: '#853403' + gas for industry CC: '#692e0a' + gas pipeline: '#ebbca0' + Gas pipeline: '#ebbca0' + # oil + oil: '#c9c9c9' + oil boiler: '#adadad' + agriculture machinery oil: '#949494' + shipping oil: "#808080" + land transport oil: '#afafaf' + # nuclear + Nuclear: '#ff8c00' + Nuclear marginal: '#ff8c00' + nuclear: '#ff8c00' + uranium: '#ff8c00' + # coal + Coal: '#545454' + coal: '#545454' + Coal marginal: '#545454' + solid: '#545454' + Lignite: '#826837' + lignite: '#826837' + Lignite marginal: '#826837' + # biomass + biogas: '#e3d37d' + biomass: '#baa741' + solid biomass: '#baa741' + solid biomass transport: '#baa741' + solid biomass for industry: '#7a6d26' + solid biomass for industry CC: '#47411c' + solid biomass for industry co2 from atmosphere: '#736412' + solid biomass for industry co2 to stored: '#47411c' + # power transmission + lines: '#6c9459' + transmission lines: '#6c9459' + electricity distribution grid: '#97ad8c' + # electricity demand + Electric load: '#110d63' + electric demand: '#110d63' + electricity: '#110d63' + industry electricity: '#2d2a66' + industry new electricity: '#2d2a66' + agriculture electricity: '#494778' + # battery + EVs + battery: '#ace37f' + battery storage: '#ace37f' + home battery: '#80c944' + home battery storage: '#80c944' + BEV charger: '#baf238' + V2G: '#e5ffa8' + land transport EV: '#baf238' + Li ion: '#baf238' + # hot water storage + water tanks: '#e69487' + hot water storage: '#e69487' + hot water charging: '#e69487' + hot water discharging: '#e69487' + # heat demand + Heat load: '#cc1f1f' + heat: '#cc1f1f' + heat demand: '#cc1f1f' + rural heat: '#ff5c5c' + central heat: '#cc1f1f' + decentral heat: '#750606' + low-temperature heat for industry: '#8f2727' + process heat: '#ff0000' + agriculture heat: '#d9a5a5' + # heat supply + heat pumps: '#2fb537' + heat pump: '#2fb537' + air heat pump: '#36eb41' + ground heat pump: '#2fb537' + Ambient: '#98eb9d' + CHP: '#8a5751' + CHP CC: '#634643' + CHP heat: '#8a5751' + CHP electric: '#8a5751' + district heating: '#e8beac' + resistive heater: '#d8f9b8' + retrofitting: '#8487e8' + building retrofitting: '#8487e8' + # hydrogen + H2 for industry: "#f073da" + H2 for shipping: "#ebaee0" + H2: '#bf13a0' + hydrogen: '#bf13a0' + SMR: '#870c71' + SMR CC: '#4f1745' + H2 liquefaction: '#d647bd' + hydrogen storage: '#bf13a0' + H2 storage: '#bf13a0' + land transport fuel cell: '#6b3161' + H2 pipeline: '#f081dc' + H2 Fuel Cell: '#c251ae' + H2 Electrolysis: '#ff29d9' + # syngas + Sabatier: '#9850ad' + methanation: '#c44ce6' + methane: '#c44ce6' + helmeth: '#e899ff' + # synfuels + Fischer-Tropsch: '#25c49a' + liquid: '#25c49a' + kerosene for aviation: '#a1ffe6' + naphtha for industry: '#57ebc4' + # co2 + CC: '#f29dae' + CCS: '#f29dae' + CO2 sequestration: '#f29dae' + DAC: '#ff5270' + co2 stored: '#f2385a' + co2: '#f29dae' + co2 vent: '#ffd4dc' + CO2 pipeline: '#f5627f' + # emissions + process emissions CC: '#000000' + process emissions: '#222222' process emissions to stored: '#444444' process emissions to atmosphere: '#888888' - process emissions: '#222222' - process emissions CC: '#484848' - oil emissions: '#666666' - land transport oil emissions: '#666666' - land transport fuel cell: '#AAAAAA' - biogas: '#800000' - solid biomass: '#DAA520' - today: '#D2691E' - shipping: '#6495ED' - electricity distribution grid: '#333333' + oil emissions: '#aaaaaa' + shipping oil emissions: "#555555" + land transport oil emissions: '#777777' + agriculture machinery oil emissions: '#333333' + # other + shipping: '#03a2ff' + power-to-heat: '#2fb537' + power-to-gas: '#c44ce6' + power-to-H2: '#ff29d9' + power-to-liquid: '#25c49a' + gas-to-power/heat: '#ee8340' + waste: '#e3d37d' + other: '#000000' diff --git a/data/district_heat_share.csv b/data/district_heat_share.csv new file mode 100644 index 00000000..5afd65c8 --- /dev/null +++ b/data/district_heat_share.csv @@ -0,0 +1,34 @@ +country,share to satisfy heat demand (residential) in percent,capacity[MWth] +AT,14,11200 +BG,16,6162 +BA,8, +HR,6.3,2221 +CZ,40, +DK,65, +FI,38,23390 +FR,5, +DE,13.8, +HU,7.92875588637399,8549 +IS,90,8079000 +IE,0.8, +IT,3,8727 +LV,73,2254 +LT,56, +MK,23.7745607009008,636 +NO,4,3400 +PL,42,54912 +PT,0.070754716981132,34 +RS,25,5821 +SI,8.86,1739 +ES,0.251589260787732,1273 +SE,50.4, +UK,2, +BY,70, +EE,52,5406 +KO,3,207 +RO,23,9962 +SK,54,15000 +NL,4,9800 +CH,4,2792 +AL,0, +ME,0, diff --git a/data/heat_load_profile_DK_AdamJensen.csv b/data/heat_load_profile_DK_AdamJensen.csv new file mode 100644 index 00000000..cb417bde --- /dev/null +++ b/data/heat_load_profile_DK_AdamJensen.csv @@ -0,0 +1,25 @@ +hour,weekday,weekend +0,0.9181438689,0.9421512708 +1,0.9172359071,0.9400891069 +2,0.9269464481,0.9461062015 +3,0.9415047932,0.9535084941 +4,0.9656299507,0.9651094993 +5,1.0221166443,0.9834676747 +6,1.1553090493,1.0124171051 +7,1.2093411031,1.0446615927 +8,1.1470295942,1.088203419 +9,1.0877191341,1.1110334576 +10,1.0418327372,1.0926752822 +11,1.0062977133,1.055488209 +12,0.9837030359,1.0251266112 +13,0.9667570278,0.9990015154 +14,0.9548320932,0.9782897278 +15,0.9509232061,0.9698167237 +16,0.9636973319,0.974288587 +17,0.9799372563,0.9886456216 +18,1.0046501848,1.0084159643 +19,1.0079452419,1.0171243296 +20,0.9860566481,0.9994722379 +21,0.9705228074,0.982761591 +22,0.9586485819,0.9698167237 +23,0.9335023778,0.9515079292 diff --git a/data/urban_percent.csv b/data/urban_percent.csv new file mode 100644 index 00000000..d57e0728 --- /dev/null +++ b/data/urban_percent.csv @@ -0,0 +1,30 @@ +AT,66 +BA,40 +BE,98 +BG,74 +CH,74 +CZ,73 +DE,75 +DK,88 +EE,68 +ES,80 +FI,84 +FR,80 +GB,83 +GR,78 +HR,59 +HU,71 +IE,63 +IT,69 +LT,67 +LU,90 +LV,67 +NL,90 +NO,80 +PL,61 +PT,63 +RO,55 +RS,56 +SE,86 +SI,50 +SK,54 diff --git a/doc/conf.py b/doc/conf.py index 1961a23b..d647e953 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -62,17 +62,17 @@ master_doc = 'index' # General information about the project. project = u'PyPSA-Eur-Sec' -copyright = u'2019-2020 Tom Brown (KIT), Marta Victoria (Aarhus University), Lisa Zeyen (KIT)' -author = u'2019-2020 Tom Brown (KIT), Marta Victoria (Aarhus University), Lisa Zeyen (KIT)' +copyright = u'2019-2021 Tom Brown (KIT, TUB), Marta Victoria (Aarhus University), Lisa Zeyen (KIT, TUB), Fabian Neumann (TUB)' +author = u'2019-2021 Tom Brown (KIT, TUB), Marta Victoria (Aarhus University), Lisa Zeyen (KIT, TUB), Fabian Neumann (TUB)' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u'0.5' +version = u'0.6' # The full version, including alpha/beta/rc tags. -release = u'0.5.0' +release = u'0.6.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/data.csv b/doc/data.csv index 8e316281..cde8c559 100644 --- a/doc/data.csv +++ b/doc/data.csv @@ -2,11 +2,11 @@ description,file/folder,licence,source JRC IDEES database,jrc-idees-2015/,CC BY 4.0,https://ec.europa.eu/jrc/en/potencia/jrc-idees urban/rural fraction,urban_percent.csv,unknown,unknown JRC biomass potentials,biomass/,unknown,https://doi.org/10.2790/39014 +JRC ENSPRESO biomass potentials,remote,CC BY 4.0,https://data.jrc.ec.europa.eu/dataset/74ed5a04-7d74-4807-9eab-b94774309d9f EEA emission statistics,eea/UNFCCC_v23.csv,EEA standard re-use policy,https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16 Eurostat Energy Balances,eurostat-energy_balances-*/,Eurostat,https://ec.europa.eu/eurostat/web/energy/data/energy-balances Swiss energy statistics from Swiss Federal Office of Energy,switzerland-sfoe/,unknown,http://www.bfe.admin.ch/themen/00526/00541/00542/02167/index.html?dossier_id=02169 BASt emobility statistics,emobility/,unknown,http://www.bast.de/DE/Verkehrstechnik/Fachthemen/v2-verkehrszaehlung/Stundenwerte.html?nn=626916 -timezone mappings,timezone_mappings.csv,CC BY 4.0,Tom Brown BDEW heating profile,heat_load_profile_BDEW.csv,unknown,https://github.com/oemof/demandlib heating profiles for Aarhus,heat_load_profile_DK_AdamJensen.csv,unknown,Adam Jensen MA thesis at Aarhus University George Lavidas wind/wave costs,WindWaveWEC_GLTB.xlsx,unknown,George Lavidas @@ -24,3 +24,6 @@ Comparative level investment,comparative_level_investment.csv,Eurostat,https://e Electricity taxes,electricity_taxes_eu.csv,Eurostat,https://appsso.eurostat.ec.europa.eu/nui/show.do?dataset=nrg_pc_204&lang=en Building topologies and corresponding standard values,tabula-calculator-calcsetbuilding.csv,unknown,https://episcope.eu/fileadmin/tabula/public/calc/tabula-calculator.xlsx Retrofitting thermal envelope costs for Germany,retro_cost_germany.csv,unkown,https://www.iwu.de/forschung/handlungslogiken/kosten-energierelevanter-bau-und-anlagenteile-bei-modernisierung/ +District heating most countries,jrc-idees-2015/,CC BY 4.0,https://ec.europa.eu/jrc/en/potencia/jrc-idees,, +District heating missing countries,district_heat_share.csv,unkown,https://www.euroheat.org/knowledge-hub/country-profiles,, + diff --git a/doc/index.rst b/doc/index.rst index 1bf307f5..c4174b52 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -29,6 +29,11 @@ heating, biomass, industry and industrial feedstocks. This completes the energy system and includes all greenhouse gas emitters except waste management, agriculture, forestry and land use. +.. note:: + More about the current model capabilities and preliminary results + can be found in `a recent presentation at EMP-E `_ + and the the following `preprint with a description of the industry sector `_. + This diagram gives an overview of the sectors and the links between them: @@ -61,9 +66,25 @@ PyPSA-Eur-Sec is the different extra_functionality required to build storage and CHP constraints. -PyPSA-Eur-Sec is designed to be imported into the open toolbox `PyPSA `_ for which `documentation `_ is available as well. +PyPSA-Eur-Sec is designed to be imported into the open toolbox `PyPSA +`_ for which `documentation `_ is +available as well. -This project is maintained by the `Energy System Modelling group `_ at the `Institute for Automation and Applied Informatics `_ at the `Karlsruhe Institute of Technology `_. The group is funded by the `Helmholtz Association `_ until 2024. Previous versions were developed by the `Renewable Energy Group `_ at `FIAS `_ to carry out simulations for the `CoNDyNet project `_, financed by the `German Federal Ministry for Education and Research (BMBF) `_ as part of the `Stromnetze Research Initiative `_. +This project is currently maintained by the `Department of Digital +Transformation in Energy Systems `_ at the +`Technical University of Berlin `_. Previous versions +were developed by the `Energy System Modelling group +`_ at the `Institute for Automation +and Applied Informatics `_ at the +`Karlsruhe Institute of Technology `_ +which was funded by the `Helmholtz Association `_, +and by the `Renewable Energy Group +`_ +at `FIAS `_ to carry out simulations for the +`CoNDyNet project `_, financed by the `German Federal +Ministry for Education and Research (BMBF) `_ +as part of the `Stromnetze Research Initiative +`_. Documentation @@ -134,7 +155,7 @@ it. Licence ======= -The code in PyPSA-Eur-Sec is released as free software under the `GPLv3 -`_, see +The code in PyPSA-Eur-Sec is released as free software under the +`MIT license `_, see `LICENSE `_. However, different licenses and terms of use may apply to the various input data. diff --git a/doc/installation.rst b/doc/installation.rst index 3ab3d328..1252771d 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -66,15 +66,15 @@ Data requirements ================= Small data files are included directly in the git repository, while -larger ones are archived in a data bundle. The data bundle's size is -around 640 MB. +larger ones are archived in a data bundle on zenodo (`10.5281/zenodo.5546517 `_). +The data bundle's size is around 640 MB. To download and extract the data bundle on the command line: .. code:: bash - - projects/pypsa-eur-sec/data % wget "https://nworbmot.org/pypsa-eur-sec-data-bundle-210418.tar.gz" - projects/pypsa-eur-sec/data % tar xvzf pypsa-eur-sec-data-bundle-210418.tar.gz +` + projects/pypsa-eur-sec/data % wget "https://zenodo.org/record/5546517/files/pypsa-eur-sec-data-bundle.tar.gz" + projects/pypsa-eur-sec/data % tar xvzf pypsa-eur-sec-data-bundle.tar.gz The data licences and sources are given in the following table. @@ -89,10 +89,8 @@ The data licences and sources are given in the following table. Set up the default configuration ================================ -First make your own copy of the ``config.yaml``. For overnight -scenarios, use ``config.default.yaml``. For a pathway optimization -with myopic foresight (which is still experimental), use -``config.myopic.yaml``. For example: +First make your own copy of the ``config.yaml`` based on + ``config.default.yaml``. For example: .. code:: bash diff --git a/doc/release_notes.rst b/doc/release_notes.rst index bd99ce16..593c420a 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -6,61 +6,192 @@ Future release ============== .. note:: - This unreleased version currently requires the master branches of PyPSA, PyPSA-Eur, and the technology-data repository. + This unreleased version currently may require the master branches of PyPSA, PyPSA-Eur, and the technology-data repository. + +PyPSA-Eur-Sec 0.6.0 (4 October 2021) +==================================== + +This release includes +improvements regarding the basic chemical production, +the addition of plastics recycling, +the addition of the agriculture, forestry and fishing sector, +more regionally resolved biomass potentials, +CO2 pipeline transport and storage, and +more options in setting exogenous transition paths, +besides many performance improvements. + +This release is known to work with `PyPSA-Eur +`_ Version 0.4.0, `Technology Data +`_ Version 0.3.0 and +`PyPSA `_ Version 0.18.0. + +Please note that the data bundle has also been updated. + + +**General** + +* With this release, we change the license from copyleft GPLv3 to the more + liberal MIT license with the consent of all contributors. + + +**New features and functionality** + +* Distinguish costs for home battery storage and inverter from utility-scale + battery costs. + +* Separate basic chemicals into HVC (high-value chemicals), chlorine, methanol and ammonia + [`#166 `_]. + +* Add option to specify reuse, primary production, and mechanical and chemical + recycling fraction of platics + [`#166 `_]. + +* Include energy demands and CO2 emissions for the agriculture, forestry and fishing sector. + It is included by default through the option ``A`` in the ``sector_opts`` wildcard. + Part of the emissions (1.A.4.c) was previously assigned to "industry non-elec" in the ``co2_totals.csv``. + Hence, excluding the agriculture sector will now lead to a tighter CO2 limit. + Energy demands are taken from the JRC IDEES database (missing countries filled with eurostat data) + and are split into + electricity (lighting, ventilation, specific electricity uses, pumping devices (electric)), + heat (specific heat uses, low enthalpy heat) + machinery oil (motor drives, farming machine drives, pumping devices (diesel)). + Heat demand is assigned at "services rural heat" buses. + Electricity demands are added to low-voltage buses. + Time series for demands are constant and distributed inside countries by population + [`#147 `_]. + +* Include today's district heating shares in myopic optimisation and add option + to specify exogenous path for district heating share increase under ``sector: + district_heating:`` [`#149 `_]. + +* Added option for hydrogen liquefaction costs for hydrogen demand in shipping. + This introduces a new ``H2 liquid`` bus at each location. It is activated via + ``sector: shipping_hydrogen_liquefaction: true``. + +* The share of shipping transformed into hydrogen fuel cell can be now defined + for different years in the ``config.yaml`` file. The carbon emission from the + remaining share is treated as a negative load on the atmospheric carbon dioxide + bus, just like aviation and land transport emissions. + +* The transformation of the Steel and Aluminium production can be now defined + for different years in the ``config.yaml`` file. + +* Include the option to alter the maximum energy capacity of a store via the + ``carrier+factor`` in the ``{sector_opts}`` wildcard. This can be useful for + sensitivity analyses. Example: ``co2 stored+e2`` multiplies the ``e_nom_max`` by + factor 2. In this example, ``e_nom_max`` represents the CO2 sequestration + potential in Europe. + +* Use `JRC ENSPRESO database `_ to + spatially disaggregate biomass potentials to PyPSA-Eur regions based on + overlaps with NUTS2 regions from ENSPRESO (proportional to area) (`#151 + `_). + +* Add option to regionally disaggregate biomass potential to individual nodes + (previously given per country, then distributed by population density within) + and allow the transport of solid biomass. The transport costs are determined + based on the `JRC-EU-Times Bioenergy report + `_ in the new optional rule + ``build_biomass_transport_costs``. Biomass transport can be activated with the + setting ``sector: biomass_transport: true``. + +* Add option to regionally resolve CO2 storage and add CO2 pipeline transport + because geological storage potential, + CO2 utilisation sites and CO2 capture sites may be separated. The CO2 network + is built from zero based on the topology of the electricity grid (greenfield). + Pipelines are assumed to be bidirectional and lossless. Furthermore, neither + retrofitting of natural gas pipelines (required pressures are too high, 80-160 + bar vs <80 bar) nor other modes of CO2 transport (by ship, road or rail) are + considered. The regional representation of CO2 is activated with the config + setting ``sector: co2_network: true`` but is deactivated by default. The + global limit for CO2 sequestration now applies to the sum of all CO2 stores + via an ``extra_functionality`` constraint. + +* The myopic option can now be used together with different clustering for the + generators and the network. The existing renewable capacities are split evenly + among the regions in every country [`#144 `_]. + +* Add optional function to use ``geopy`` to locate entries of the Hotmaps + database of industrial sites with missing location based on city and country, + which reduces missing entries by half. It can be activated by setting + ``industry: hotmaps_locate_missing: true``, takes a few minutes longer, and + should only be used if spatial resolution is coarser than city level. + + +**Performance and Structure** * Extended use of ``multiprocessing`` for much better performance (from up to 20 minutes to less than one minute). -* Compatibility with ``atlite>=0.2``. Older versions of ``atlite`` will no longer work. + * Handle most input files (or base directories) via ``snakemake.input``. + * Use of ``mock_snakemake`` from PyPSA-Eur. -* Update ``solve_network`` rule to match implementation in PyPSA-Eur by using ``n.ilopf()`` and remove outdated code using ``pyomo``. - Allows the new setting to skip iterated impedance updates with ``solving: options: skip_iterations: true``. + +* Update ``solve_network`` rule to match implementation in PyPSA-Eur by using + ``n.ilopf()`` and remove outdated code using ``pyomo``. + Allows the new setting to skip iterated impedance updates with ``solving: + options: skip_iterations: true``. + * The component attributes that are to be overridden are now stored in the folder ``data/override_component_attrs`` analogous to ``pypsa/component_attrs``. This reduces verbosity and also allows circumventing the ``n.madd()`` hack for individual components with non-default attributes. This data is also tracked in the Snakefile. - A function ``helper.override_component_attrs`` was added that loads this data - and can pass the overridden component attributes into ``pypsa.Network()``: - - >>> from helper import override_component_attrs - >>> overrides = override_component_attrs(snakemake.input.overrides) - >>> n = pypsa.Network("mynetwork.nc", override_component_attrs=overrides) - + and can pass the overridden component attributes into ``pypsa.Network()``. + * Add various parameters to ``config.default.yaml`` which were previously hardcoded inside the scripts (e.g. energy reference years, BEV settings, solar thermal collector models, geomap colours). + * Removed stale industry demand rules ``build_industrial_energy_demand_per_country`` and ``build_industrial_demand``. These are superseded with more regionally resolved rules. + * Use simpler and shorter ``gdf.sjoin()`` function to allocate industrial sites - from the Hotmaps database to onshore regions. - + from the Hotmaps database to onshore regions. This change also fixes a bug: The previous version allocated sites to the closest bus, but at country borders (where Voronoi cells are distorted by the borders), this had resulted in e.g. a Spanish site close to the French border being wrongly allocated to the French bus if the bus center was closer. -* Bugfix: Corrected calculation of "gas for industry" carbon capture efficiency. + * Retrofitting rule is now only triggered if endogeneously optimised. + * Show progress in build rules with ``tqdm`` progress bars. + * Reduced verbosity of ``Snakefile`` through directory prefixes. + * Improve legibility of ``config.default.yaml`` and remove unused options. -* Add optional function to use ``geopy`` to locate entries of the Hotmaps database of industrial sites - with missing location based on city and country, which reduces missing entries by half. It can be - activated by setting ``industry: hotmaps_locate_missing: true``, takes a few minutes longer, - and should only be used if spatial resolution is coarser than city level. + * Use the country-specific time zone mappings from ``pytz`` rather than a manual mapping. + * A function ``add_carrier_buses()`` was added to the ``prepare_network`` rule to reduce code duplication. + * In the ``prepare_network`` rule the cost and potential adjustment was moved into an own function ``maybe_adjust_costs_and_potentials()``. -* Use ``matplotlibrc`` to set the default plotting style and backend``. -* Added benchmark files for each rule. -* Implements changes to ``n.snapshot_weightings`` in upcoming PyPSA version (cf. `PyPSA/#227 `_). -* New dependencies: ``tqdm``, ``atlite>=0.2.4``, ``pytz`` and ``geopy`` (optional). - These are included in the environment specifications of PyPSA-Eur. -* Consistent use of ``__main__`` block and further unspecific code cleaning. -* Distinguish costs for home battery storage and inverter from utility-scale battery costs. +* Use ``matplotlibrc`` to set the default plotting style and backend. + +* Added benchmark files for each rule. + +* Consistent use of ``__main__`` block and further unspecific code cleaning. + +* Updated data bundle and moved data bundle to zenodo.org (`10.5281/zenodo.5546517 `_). + + +**Bugfixes and Compatibility** + +* Compatibility with ``atlite>=0.2``. Older versions of ``atlite`` will no longer work. + +* Corrected calculation of "gas for industry" carbon capture efficiency. + +* Implemented changes to ``n.snapshot_weightings`` in PyPSA v0.18.0. + +* Compatibility with ``xarray`` version 0.19. + +* New dependencies: ``tqdm``, ``atlite>=0.2.4``, ``pytz`` and ``geopy`` (optional). + These are included in the environment specifications of PyPSA-Eur v0.4.0. + +Many thanks to all who contributed to this release! PyPSA-Eur-Sec 0.5.0 (21st May 2021) @@ -242,4 +373,4 @@ To make a new release of the data bundle, make an archive of the files in ``data .. code:: bash - data % tar pczf pypsa-eur-sec-data-bundle-YYMMDD.tar.gz eea/UNFCCC_v23.csv switzerland-sfoe biomass eurostat-energy_balances-* jrc-idees-2015 emobility urban_percent.csv timezone_mappings.csv heat_load_profile_DK_AdamJensen.csv WindWaveWEC_GLTB.xlsx myb1-2017-nitro.xls Industrial_Database.csv retro/tabula-calculator-calcsetbuilding.csv + data % tar pczf pypsa-eur-sec-data-bundle.tar.gz eea/UNFCCC_v23.csv switzerland-sfoe biomass eurostat-energy_balances-* jrc-idees-2015 emobility WindWaveWEC_GLTB.xlsx myb1-2017-nitro.xls Industrial_Database.csv retro/tabula-calculator-calcsetbuilding.csv nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson diff --git a/doc/spatial_resolution.rst b/doc/spatial_resolution.rst index 1be9f3ad..83a33f73 100644 --- a/doc/spatial_resolution.rst +++ b/doc/spatial_resolution.rst @@ -44,11 +44,13 @@ Hydrogen network: nodal. Methane network: single node for Europe, since future demand is so low and no bottlenecks are expected. -Solid biomass: single node for Europe, until transport costs can be -incorporated. +Solid biomass: choice between single node for Europe and nodal where biomass +potential is regionally disaggregated (currently given per country, +then distributed by population density within) +and transport of solid biomass is possible. CO2: single node for Europe, but a transport and storage cost is added for -sequestered CO2. +sequestered CO2. Optionally: nodal, with CO2 transport via pipelines. Liquid hydrocarbons: single node for Europe, since transport costs for liquids are low. diff --git a/doc/supply_demand.rst b/doc/supply_demand.rst index 77317094..3ab00d8e 100644 --- a/doc/supply_demand.rst +++ b/doc/supply_demand.rst @@ -43,7 +43,7 @@ Heat demand is split into: * ``urban central``: large-scale district heating networks in urban areas with dense heat demand * ``residential/services urban decentral``: heating for individual buildings in urban areas -* ``residential/services rural``: heating for individual buildings in rural areas +* ``residential/services rural``: heating for individual buildings in rural areas, agriculture heat uses Heat supply @@ -183,13 +183,13 @@ Solid biomass provides process heat up to 500 Celsius in industry, as well as fe Solid biomass supply ===================== -Only wastes and residues from the JRC biomass dataset. +Only wastes and residues from the JRC ENSPRESO biomass dataset. Oil product demand ===================== -Transport fuels and naphtha as a feedstock for the chemicals industry. +Transport fuels, agriculture machinery and naphtha as a feedstock for the chemicals industry. Oil product supply ====================== diff --git a/graphics/multisector_figure.pdf b/graphics/multisector_figure.pdf index e49994d7..35bbaf1d 100644 Binary files a/graphics/multisector_figure.pdf and b/graphics/multisector_figure.pdf differ diff --git a/graphics/multisector_figure.png b/graphics/multisector_figure.png index e5c17c73..415e7e6d 100644 Binary files a/graphics/multisector_figure.png and b/graphics/multisector_figure.png differ diff --git a/graphics/multisector_figure.svg b/graphics/multisector_figure.svg index 65fcad07..ece433df 100644 --- a/graphics/multisector_figure.svg +++ b/graphics/multisector_figure.svg @@ -15,7 +15,7 @@ id="svg7114" version="1.1" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" - sodipodi:docname="20200223_multisector_figure.svg"> + sodipodi:docname="multisector_figure.svg"> + + + image/svg+xml - + @@ -1630,14 +1645,6 @@ inkscape:groupmode="layer" id="layer1" transform="translate(-27.752361,-374.2016)"> -       - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Wind & Solar PV - - - - Hydroelectricity - - - - Biogas - - - - Fossil gas - - - - Other biomass - - - - Atmosphere - - - - Fossil oil - - - - Electricity - - - - Hydrogen - - - - Methane - - - - Carbon Dioxide - - - - Liquid hydrocarbons - - - - - Electric devices - - - - - Resistive heaters - - - - Heat pumps - - - - - Gas boilers - - - - - CHP - - - - Electric - - - - Fuel cell - - - - - Internalcombustion - - - - - Industry - - Heating Transport - - - - S O U R C E S G R I D S & S T O R A G E D E M A N D Electrolysis Fuel cell Methanation Steam reforming Direct air capture Carbon capture Fischer-Tropsch     - + +     + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wind & Solar PV + + + + Hydroelectricity + + + + Biogas + + + + Fossil gas + + + + Other biomass + + + + Atmosphere + + + + Fossil oil + + + + Electricity + + + + Hydrogen + + + + Methane + + + + Carbon Dioxide + + + + Liquid hydrocarbons + + + + + Electric devices + + + + + Resistive heaters + + + + Heat pumps + + + + + Gas boilers + + + + + CHP + + + + Electric + + + + Fuel cell + + + + + Internalcombustion + + + + + Industry + + Heating Transport + + + + S O U R C E S G R I D S & S T O R A G E D E M A N D Electrolysis Fuel cell Methanation Steam reforming Direct air capture Carbon capture Fischer-Tropsch   - + x="30.698057" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Calibri;-inkscape-font-specification:Calibri;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve">  + + + diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 47c31c0e..bb35e378 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -28,7 +28,7 @@ def add_build_year_to_new_assets(n, baseyear): # Give assets with lifetimes and no build year the build year baseyear for c in n.iterate_components(["Link", "Generator", "Store"]): - assets = c.df.index[~c.df.lifetime.isna() & c.df.build_year.isna()] + assets = c.df.index[~c.df.lifetime.isna() & c.df.build_year==0] c.df.loc[assets, "build_year"] = baseyear # add -baseyear to name @@ -60,7 +60,7 @@ def add_existing_renewables(df_agg): } for tech in ['solar', 'onwind', 'offwind']: - + carrier = carriers[tech] df = pd.read_csv(snakemake.input[f"existing_{tech}"], index_col=0).fillna(0.) @@ -112,9 +112,9 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas Parameters ---------- n : pypsa.Network - grouping_years : + grouping_years : intervals to group existing capacities - costs : + costs : to read lifetime to estimate YearDecomissioning baseyear : int """ @@ -155,6 +155,11 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas # assign clustered bus busmap_s = pd.read_csv(snakemake.input.busmap_s, index_col=0, squeeze=True) busmap = pd.read_csv(snakemake.input.busmap, index_col=0, squeeze=True) + + inv_busmap = {} + for k, v in busmap.iteritems(): + inv_busmap[v] = inv_busmap.get(v, []) + [k] + clustermaps = busmap_s.map(busmap) clustermaps.index = clustermaps.index.astype(int) @@ -192,24 +197,54 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas capacity = capacity[capacity > snakemake.config['existing_capacities']['threshold_capacity']] if generator in ['solar', 'onwind', 'offwind']: - - rename = {"offwind": "offwind-ac"} - p_max_pu=n.generators_t.p_max_pu[capacity.index + ' ' + rename.get(generator, generator) + '-' + str(baseyear)] - - n.madd("Generator", - capacity.index, - suffix=' ' + generator +"-"+ str(grouping_year), - bus=capacity.index, - carrier=generator, - p_nom=capacity, - marginal_cost=costs.at[generator, 'VOM'], - capital_cost=costs.at[generator, 'fixed'], - efficiency=costs.at[generator, 'efficiency'], - p_max_pu=p_max_pu.rename(columns=n.generators.bus), - build_year=grouping_year, - lifetime=costs.at[generator, 'lifetime'] - ) + suffix = '-ac' if generator == 'offwind' else '' + name_suffix = f' {generator}{suffix}-{baseyear}' + + if 'm' in snakemake.wildcards.clusters: + + for ind in capacity.index: + + # existing capacities are split evenly among regions in every country + inv_ind = [i for i in inv_busmap[ind]] + + # for offshore the spliting only inludes coastal regions + inv_ind = [i for i in inv_ind if (i + name_suffix) in n.generators.index] + + p_max_pu = n.generators_t.p_max_pu[[i + name_suffix for i in inv_ind]] + p_max_pu.columns=[i + name_suffix for i in inv_ind ] + + n.madd("Generator", + [i + name_suffix for i in inv_ind], + bus=ind, + carrier=generator, + p_nom=capacity[ind] / len(inv_ind), # split among regions in a country + marginal_cost=costs.at[generator,'VOM'], + capital_cost=costs.at[generator,'fixed'], + efficiency=costs.at[generator, 'efficiency'], + p_max_pu=p_max_pu, + build_year=grouping_year, + lifetime=costs.at[generator,'lifetime'] + ) + + else: + + p_max_pu = n.generators_t.p_max_pu[capacity.index + name_suffix] + + n.madd("Generator", + capacity.index, + suffix=' ' + generator +"-"+ str(grouping_year), + bus=capacity.index, + carrier=generator, + p_nom=capacity, + marginal_cost=costs.at[generator, 'VOM'], + capital_cost=costs.at[generator, 'fixed'], + efficiency=costs.at[generator, 'efficiency'], + p_max_pu=p_max_pu.rename(columns=n.generators.bus), + build_year=grouping_year, + lifetime=costs.at[generator, 'lifetime'] + ) + else: n.madd("Link", @@ -268,7 +303,7 @@ def add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years df.fillna(0., inplace=True) # convert GW to MW - df *= 1e3 + df *= 1e3 cc = pd.read_csv(snakemake.input.country_codes, index_col=0) @@ -327,7 +362,7 @@ def add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years efficiency = cop[heat_pump_type][nodes[name]] else: efficiency = costs.at[costs_name, 'efficiency'] - + for i, grouping_year in enumerate(grouping_years): if int(grouping_year) + default_lifetime <= int(baseyear): @@ -378,7 +413,7 @@ def add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years build_year=int(grouping_year), lifetime=costs.at[name_type + ' gas boiler', 'lifetime'] ) - + n.madd("Link", nodes[name], suffix=f" {name} oil boiler-{grouping_year}", @@ -410,7 +445,8 @@ if __name__ == "__main__": simpl='', clusters=45, lv=1.0, - sector_opts='Co2L0-168H-T-H-B-I-solar3-dist1', + opts='', + sector_opts='Co2L0-168H-T-H-B-I-solar+p3-dist1', planning_horizons=2020, ) diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index f02c9093..68d87808 100644 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -1,55 +1,194 @@ import pandas as pd - -rename = {"UK" : "GB", "BH" : "BA"} +import geopandas as gpd -def build_biomass_potentials(): +def build_nuts_population_data(year=2013): - config = snakemake.config['biomass'] - year = config["year"] - scenario = config["scenario"] + pop = pd.read_csv( + snakemake.input.nuts3_population, + sep=r'\,| \t|\t', + engine='python', + na_values=[":"], + index_col=1 + )[str(year)] + + # only countries + pop.drop("EU28", inplace=True) - df = pd.read_excel(snakemake.input.jrc_potentials, - "Potentials (PJ)", - index_col=[0,1]) + # mapping from Cantons to NUTS3 + cantons = pd.read_csv(snakemake.input.swiss_cantons) + cantons = cantons.set_index(cantons.HASC.str[3:]).NUTS + cantons = cantons.str.pad(5, side='right', fillchar='0') - df.rename(columns={"Unnamed: 18": "Municipal waste"}, inplace=True) - df.drop(columns="Total", inplace=True) - df.replace("-", 0., inplace=True) + # get population by NUTS3 + swiss = pd.read_excel(snakemake.input.swiss_population, skiprows=3, index_col=0).loc["Residents in 1000"] + swiss = swiss.rename(cantons).filter(like="CH") - column = df.iloc[:,0] - countries = column.where(column.str.isalpha()).pad() - countries = [rename.get(ct, ct) for ct in countries] - countries_i = pd.Index(countries, name='country') - df.set_index(countries_i, append=True, inplace=True) + # aggregate also to higher order NUTS levels + swiss = [swiss.groupby(swiss.index.str[:i]).sum() for i in range(2, 6)] - df.drop(index='MS', level=0, inplace=True) + # merge Europe + Switzerland + pop = pd.DataFrame(pop.append(swiss), columns=["total"]) + + # add missing manually + pop["AL"] = 2893 + pop["BA"] = 3871 + pop["RS"] = 7210 + + pop["ct"] = pop.index.str[:2] + + return pop - # convert from PJ to MWh - df = df / 3.6 * 1e6 - df.to_csv(snakemake.output.biomass_potentials_all) +def enspreso_biomass_potentials(year=2020, scenario="ENS_Low"): + """ + Loads the JRC ENSPRESO biomass potentials. + + Parameters + ---------- + year : int + The year for which potentials are to be taken. + Can be {2010, 2020, 2030, 2040, 2050}. + scenario : str + The scenario. Can be {"ENS_Low", "ENS_Med", "ENS_High"}. + + Returns + ------- + pd.DataFrame + Biomass potentials for given year and scenario + in TWh/a by commodity and NUTS2 region. + """ - # solid biomass includes: - # Primary agricultural residues (MINBIOAGRW1), - # Forestry energy residue (MINBIOFRSF1), - # Secondary forestry residues (MINBIOWOOW1), - # Secondary Forestry residues – sawdust (MINBIOWOO1a)', - # Forestry residues from landscape care biomass (MINBIOFRSF1a), - # Municipal waste (MINBIOMUN1)', + glossary = pd.read_excel( + str(snakemake.input.enspreso_biomass), + sheet_name="Glossary", + usecols="B:D", + skiprows=1, + index_col=0 + ) + + df = pd.read_excel( + str(snakemake.input.enspreso_biomass), + sheet_name="ENER - NUTS2 BioCom E", + usecols="A:H" + ) - # biogas includes: - # Manure biomass potential (MINBIOGAS1), - # Sludge biomass (MINBIOSLU1), + df["group"] = df["E-Comm"].map(glossary.group) + df["commodity"] = df["E-Comm"].map(glossary.description) - df = df.loc[year, scenario, :] + to_rename = { + "NUTS2 Potential available by Bio Commodity": "potential", + "NUST2": "NUTS2", + } + df.rename(columns=to_rename, inplace=True) + + # fill up with NUTS0 if NUTS2 is not given + df.NUTS2 = df.apply(lambda x: x.NUTS0 if x.NUTS2 == '-' else x.NUTS2, axis=1) - grouper = {v: k for k, vv in config["classes"].items() for v in vv} - df = df.groupby(grouper, axis=1).sum() + # convert PJ to TWh + df.potential /= 3.6 + df.Unit = "TWh/a" - df.index.name = "MWh/a" + dff = df.query("Year == @year and Scenario == @scenario") - df.to_csv(snakemake.output.biomass_potentials) + bio = dff.groupby(["NUTS2", "commodity"]).potential.sum().unstack() + + # currently Serbia and Kosovo not split, so aggregate + bio.loc["RS"] += bio.loc["XK"] + bio.drop("XK", inplace=True) + + return bio + + +def disaggregate_nuts0(bio): + """ + Some commodities are only given on NUTS0 level. + These are disaggregated here using the NUTS2 + population as distribution key. + + Parameters + ---------- + bio : pd.DataFrame + from enspreso_biomass_potentials() + + Returns + ------- + pd.DataFrame + """ + + pop = build_nuts_population_data() + + # get population in nuts2 + pop_nuts2 = pop.loc[pop.index.str.len() == 4] + by_country = pop_nuts2.total.groupby(pop_nuts2.ct).sum() + pop_nuts2["fraction"] = pop_nuts2.total / pop_nuts2.ct.map(by_country) + + # distribute nuts0 data to nuts2 by population + bio_nodal = bio.loc[pop_nuts2.ct] + bio_nodal.index = pop_nuts2.index + bio_nodal = bio_nodal.mul(pop_nuts2.fraction, axis=0) + + # update inplace + bio.update(bio_nodal) + + return bio + + +def build_nuts2_shapes(): + """ + - load NUTS2 geometries + - add RS, AL, BA country shapes (not covered in NUTS 2013) + - consistently name ME, MK + """ + + nuts2 = gpd.GeoDataFrame(gpd.read_file(snakemake.input.nuts2).set_index('id').geometry) + + countries = gpd.read_file(snakemake.input.country_shapes).set_index('name') + missing = countries.loc[["AL", "RS", "BA"]] + nuts2.rename(index={"ME00": "ME", "MK00": "MK"}, inplace=True) + + return nuts2.append(missing) + + +def area(gdf): + """Returns area of GeoDataFrame geometries in square kilometers.""" + return gdf.to_crs(epsg=3035).area.div(1e6) + + +def convert_nuts2_to_regions(bio_nuts2, regions): + """ + Converts biomass potentials given in NUTS2 to PyPSA-Eur regions based on the + overlay of both GeoDataFrames in proportion to the area. + + Parameters + ---------- + bio_nuts2 : gpd.GeoDataFrame + JRC ENSPRESO biomass potentials indexed by NUTS2 shapes. + regions : gpd.GeoDataFrame + PyPSA-Eur clustered onshore regions + + Returns + ------- + gpd.GeoDataFrame + """ + + # calculate area of nuts2 regions + bio_nuts2["area_nuts2"] = area(bio_nuts2) + + overlay = gpd.overlay(regions, bio_nuts2) + + # calculate share of nuts2 area inside region + overlay["share"] = area(overlay) / overlay["area_nuts2"] + + # multiply all nuts2-level values with share of nuts2 inside region + adjust_cols = overlay.columns.difference({"name", "area_nuts2", "geometry", "share"}) + overlay[adjust_cols] = overlay[adjust_cols].multiply(overlay["share"], axis=0) + + bio_regions = overlay.groupby("name").sum() + + bio_regions.drop(["area_nuts2", "share"], axis=1, inplace=True) + + return bio_regions if __name__ == "__main__": @@ -57,12 +196,28 @@ if __name__ == "__main__": from helper import mock_snakemake snakemake = mock_snakemake('build_biomass_potentials') + config = snakemake.config['biomass'] + year = config["year"] + scenario = config["scenario"] - # This is a hack, to be replaced once snakemake is unicode-conform + enspreso = enspreso_biomass_potentials(year, scenario) - solid_biomass = snakemake.config['biomass']['classes']['solid biomass'] - if 'Secondary Forestry residues sawdust' in solid_biomass: - solid_biomass.remove('Secondary Forestry residues sawdust') - solid_biomass.append('Secondary Forestry residues – sawdust') + enspreso = disaggregate_nuts0(enspreso) - build_biomass_potentials() + nuts2 = build_nuts2_shapes() + + df_nuts2 = gpd.GeoDataFrame(nuts2.geometry).join(enspreso) + + regions = gpd.read_file(snakemake.input.regions_onshore) + + df = convert_nuts2_to_regions(df_nuts2, regions) + + df.to_csv(snakemake.output.biomass_potentials_all) + + grouper = {v: k for k, vv in config["classes"].items() for v in vv} + df = df.groupby(grouper, axis=1).sum() + + df *= 1e6 # TWh/a to MWh/a + df.index.name = "MWh/a" + + df.to_csv(snakemake.output.biomass_potentials) diff --git a/scripts/build_biomass_transport_costs.py b/scripts/build_biomass_transport_costs.py new file mode 100644 index 00000000..aaec215b --- /dev/null +++ b/scripts/build_biomass_transport_costs.py @@ -0,0 +1,90 @@ +""" +Reads biomass transport costs for different countries of the JRC report + + "The JRC-EU-TIMES model. + Bioenergy potentials + for EU and neighbouring countries." + (2015) + +converts them from units 'EUR per km/ton' -> 'EUR/ (km MWh)' + +assuming as an approximation energy content of wood pellets + +@author: bw0928 +""" + +import pandas as pd +import tabula as tbl + +ENERGY_CONTENT = 4.8 # unit MWh/t (wood pellets) + +def get_countries(): + + pandas_options = dict( + skiprows=range(6), + header=None, + index_col=0 + ) + + return tbl.read_pdf( + str(snakemake.input.transport_cost_data), + pages="145", + multiple_tables=False, + pandas_options=pandas_options + )[0].index + + +def get_cost_per_tkm(page, countries): + + pandas_options = dict( + skiprows=range(6), + header=0, + sep=' |,', + engine='python', + index_col=False, + ) + + sc = tbl.read_pdf( + str(snakemake.input.transport_cost_data), + pages=page, + multiple_tables=False, + pandas_options=pandas_options + )[0] + sc.index = countries + sc.columns = sc.columns.str.replace("€", "EUR") + + return sc + + +def build_biomass_transport_costs(): + + countries = get_countries() + + sc1 = get_cost_per_tkm(146, countries) + sc2 = get_cost_per_tkm(147, countries) + + # take mean of both supply chains + to_concat = [sc1["EUR/km/ton"], sc2["EUR/km/ton"]] + transport_costs = pd.concat(to_concat, axis=1).mean(axis=1) + + # convert tonnes to MWh + transport_costs /= ENERGY_CONTENT + transport_costs.name = "EUR/km/MWh" + + # rename country names + to_rename = { + "UK": "GB", + "XK": "KO", + "EL": "GR" + } + transport_costs.rename(to_rename, inplace=True) + + # add missing Norway with data from Sweden + transport_costs["NO"] = transport_costs["SE"] + + transport_costs.to_csv(snakemake.output[0]) + + +if __name__ == "__main__": + + build_biomass_transport_costs() diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index aec1c61b..3f376b0c 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -117,6 +117,7 @@ to_ipcc = { "total energy": "1 - Energy", "industrial processes": "2 - Industrial Processes and Product Use", "agriculture": "3 - Agriculture", + "agriculture, forestry and fishing": '1.A.4.c - Agriculture/Forestry/Fishing', "LULUCF": "4 - Land Use, Land-Use Change and Forestry", "waste management": "5 - Waste management", "other": "6 - Other Sector", @@ -182,7 +183,7 @@ def idees_per_country(ct, year): ct_idees = idees_rename.get(ct, ct) fn_residential = f"{base_dir}/JRC-IDEES-2015_Residential_{ct_idees}.xlsx" - fn_services = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx" + fn_tertiary = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx" fn_transport = f"{base_dir}/JRC-IDEES-2015_Transport_{ct_idees}.xlsx" # residential @@ -212,9 +213,15 @@ def idees_per_country(ct, year): assert df.index[47] == "Electricity" ct_totals["electricity residential"] = df[47] + assert df.index[46] == "Derived heat" + ct_totals["derived heat residential"] = df[46] + + assert df.index[50] == 'Thermal uses' + ct_totals["thermal uses residential"] = df[50] + # services - df = pd.read_excel(fn_services, "SER_hh_fec", index_col=0)[year] + df = pd.read_excel(fn_tertiary, "SER_hh_fec", index_col=0)[year] ct_totals["total services space"] = df["Space heating"] @@ -231,7 +238,7 @@ def idees_per_country(ct, year): assert df.index[31] == "Electricity" ct_totals["electricity services cooking"] = df[31] - df = pd.read_excel(fn_services, "SER_summary", index_col=0)[year] + df = pd.read_excel(fn_tertiary, "SER_summary", index_col=0)[year] row = "Energy consumption by fuel - Eurostat structure (ktoe)" ct_totals["total services"] = df[row] @@ -239,6 +246,41 @@ def idees_per_country(ct, year): assert df.index[50] == "Electricity" ct_totals["electricity services"] = df[50] + assert df.index[49] == "Derived heat" + ct_totals["derived heat services"] = df[49] + + assert df.index[53] == 'Thermal uses' + ct_totals["thermal uses services"] = df[53] + + + # agriculture, forestry and fishing + + start = "Detailed split of energy consumption (ktoe)" + end = "Market shares of energy uses (%)" + + df = pd.read_excel(fn_tertiary, "AGR_fec", index_col=0).loc[start:end, year] + + rows = [ + "Lighting", + "Ventilation", + "Specific electricity uses", + "Pumping devices (electric)" + ] + ct_totals["total agriculture electricity"] = df[rows].sum() + + rows = ["Specific heat uses", "Low enthalpy heat"] + ct_totals["total agriculture heat"] = df[rows].sum() + + rows = [ + "Motor drives", + "Farming machine drives (diesel oil incl. biofuels)", + "Pumping devices (diesel oil incl. biofuels)", + ] + ct_totals["total agriculture machinery"] = df[rows].sum() + + row = "Agriculture, forestry and fishing" + ct_totals["total agriculture"] = df[row] + # transport df = pd.read_excel(fn_transport, "TrRoad_ene", index_col=0)[year] @@ -342,6 +384,7 @@ def build_idees(countries, year): with mp.Pool(processes=nprocesses) as pool: totals_list = list(tqdm(pool.imap(func, countries), **tqdm_kwargs)) + totals = pd.concat(totals_list, axis=1) # convert ktoe to TWh @@ -351,6 +394,13 @@ def build_idees(countries, year): # convert TWh/100km to kWh/km totals.loc["passenger car efficiency"] *= 10 + # district heating share + district_heat = totals.loc[["derived heat residential", + "derived heat services"]].sum() + total_heat = totals.loc[["thermal uses residential", + "thermal uses services"]].sum() + totals.loc["district heat share"] = district_heat.div(total_heat) + return totals.T @@ -493,7 +543,7 @@ def build_energy_totals(countries, eurostat, swiss, idees): for purpose in ["passenger", "freight"]: attrs = [f"total domestic aviation {purpose}", f"total international aviation {purpose}"] - df.loc[missing, f"total aviation {purpose}"] = df.loc[missing, attrs].sum(axis=1) + df.loc[missing, f"total aviation {purpose}"] = df.loc[missing, attrs].sum(axis=1) if "BA" in df.index: # fill missing data for BA (services and road energy data) @@ -502,6 +552,14 @@ def build_energy_totals(countries, eurostat, swiss, idees): ratio = df.at["BA", "total residential"] / df.at["RS", "total residential"] df.loc['BA', missing] = ratio * df.loc["RS", missing] + # Missing district heating share + dh_share = pd.read_csv(snakemake.input.district_heat_share, + index_col=0, usecols=[0, 1]) + # make conservative assumption and take minimum from both data sets + df["district heat share"] = (pd.concat([df["district heat share"], + dh_share.reindex(index=df.index)/100], + axis=1).min(axis=1)) + return df @@ -540,10 +598,13 @@ def build_eea_co2(year=1990): "international aviation", "domestic navigation", "international navigation", + "agriculture, forestry and fishing" ] emissions["industrial non-elec"] = emissions["total energy"] - emissions[to_subtract].sum(axis=1) - to_drop = ["total energy", "total wL", "total woL"] + emissions["agriculture"] += emissions["agriculture, forestry and fishing"] + + to_drop = ["total energy", "total wL", "total woL", "agriculture, forestry and fishing"] emissions.drop(columns=to_drop, inplace=True) # convert from Gg to Mt @@ -588,7 +649,7 @@ def build_co2_totals(countries, eea_co2, eurostat_co2): # does not include industrial process emissions or fuel processing/refining "industrial non-elec": (ct, "+", "Industry"), # does not include non-energy emissions - "agriculture": (ct, "+", "+", "Agriculture / Forestry"), + "agriculture": (eurostat_co2.index.get_level_values(0) == ct) & eurostat_co2.index.isin(["Agriculture / Forestry", "Fishing"], level=3), } for i, mi in mappings.items(): diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 1d906b24..0adf84e7 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -103,6 +103,7 @@ def add_ammonia_energy_demand(demand): demand['Basic chemicals (without ammonia)'] = demand["Basic chemicals"] - demand["Ammonia"] demand['Basic chemicals (without ammonia)'].clip(lower=0, inplace=True) + demand.drop(columns='Basic chemicals', inplace=True) return demand @@ -114,6 +115,11 @@ def add_non_eu28_industrial_energy_demand(demand): fn = snakemake.input.industrial_production_per_country production = pd.read_csv(fn, index_col=0) / 1e3 + #recombine HVC, Chlorine and Methanol to Basic chemicals (without ammonia) + chemicals = ["HVC", "Chlorine", "Methanol"] + production["Basic chemicals (without ammonia)"] = production[chemicals].sum(axis=1) + production.drop(columns=chemicals, inplace=True) + eu28_production = production.loc[eu28].sum() eu28_energy = demand.groupby(level=1).sum() eu28_averages = eu28_energy / eu28_production diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 1754752a..eadfb224 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -179,8 +179,8 @@ def industry_production(countries): return demand -def add_ammonia_demand_separately(demand): - """Include ammonia demand separately and remove ammonia from basic chemicals.""" +def separate_basic_chemicals(demand): + """Separate basic chemicals into ammonia, chlorine, methanol and HVC.""" ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) @@ -189,7 +189,7 @@ def add_ammonia_demand_separately(demand): print("Following countries have no ammonia demand:", missing) - demand.insert(2, "Ammonia", 0.) + demand["Ammonia"] = 0. demand.loc[there, "Ammonia"] = ammonia.loc[there, str(year)] @@ -198,9 +198,13 @@ def add_ammonia_demand_separately(demand): # EE, HR and LT got negative demand through subtraction - poor data demand['Basic chemicals'].clip(lower=0., inplace=True) - to_rename = {"Basic chemicals": "Basic chemicals (without ammonia)"} - demand.rename(columns=to_rename, inplace=True) + # assume HVC, methanol, chlorine production proportional to non-ammonia basic chemicals + distribution_key = demand["Basic chemicals"] / demand["Basic chemicals"].sum() + demand["HVC"] = config["HVC_production_today"] * 1e3 * distribution_key + demand["Chlorine"] = config["chlorine_production_today"] * 1e3 * distribution_key + demand["Methanol"] = config["methanol_production_today"] * 1e3 * distribution_key + demand.drop(columns=["Basic chemicals"], inplace=True) if __name__ == '__main__': if 'snakemake' not in globals(): @@ -211,12 +215,14 @@ if __name__ == '__main__': year = snakemake.config['industry']['reference_year'] + config = snakemake.config["industry"] + jrc_dir = snakemake.input.jrc eurostat_dir = snakemake.input.eurostat demand = industry_production(countries) - add_ammonia_demand_separately(demand) + separate_basic_chemicals(demand) fn = snakemake.output.industrial_production_per_country demand.to_csv(fn, float_format='%.2f') diff --git a/scripts/build_industrial_production_per_country_tomorrow.py b/scripts/build_industrial_production_per_country_tomorrow.py index 767779f8..ccf31839 100644 --- a/scripts/build_industrial_production_per_country_tomorrow.py +++ b/scripts/build_industrial_production_per_country_tomorrow.py @@ -2,6 +2,8 @@ import pandas as pd +from prepare_sector_network import get + if __name__ == '__main__': if 'snakemake' not in globals(): from helper import mock_snakemake @@ -9,31 +11,42 @@ if __name__ == '__main__': config = snakemake.config["industry"] + investment_year = int(snakemake.wildcards.planning_horizons) + fn = snakemake.input.industrial_production_per_country production = pd.read_csv(fn, index_col=0) keys = ["Integrated steelworks", "Electric arc"] total_steel = production[keys].sum(axis=1) + st_primary_fraction = get(config["St_primary_fraction"], investment_year) + dri_fraction = get(config["DRI_fraction"], investment_year) int_steel = production["Integrated steelworks"].sum() - fraction_persistent_primary = config["St_primary_fraction"] * total_steel.sum() / int_steel + fraction_persistent_primary = st_primary_fraction * total_steel.sum() / int_steel - dri = fraction_persistent_primary * production["Integrated steelworks"] + dri = dri_fraction * fraction_persistent_primary * production["Integrated steelworks"] production.insert(2, "DRI + Electric arc", dri) - production["Electric arc"] = total_steel - production["DRI + Electric arc"] - production["Integrated steelworks"] = 0. + not_dri = (1 - dri_fraction) + production["Integrated steelworks"] = not_dri * fraction_persistent_primary * production["Integrated steelworks"] + production["Electric arc"] = total_steel - production["DRI + Electric arc"] - production["Integrated steelworks"] keys = ["Aluminium - primary production", "Aluminium - secondary production"] total_aluminium = production[keys].sum(axis=1) key_pri = "Aluminium - primary production" key_sec = "Aluminium - secondary production" - fraction_persistent_primary = config["Al_primary_fraction"] * total_aluminium.sum() / production[key_pri].sum() + + al_primary_fraction = get(config["Al_primary_fraction"], investment_year) + fraction_persistent_primary = al_primary_fraction * total_aluminium.sum() / production[key_pri].sum() + production[key_pri] = fraction_persistent_primary * production[key_pri] production[key_sec] = total_aluminium - production[key_pri] - production["Basic chemicals (without ammonia)"] *= config['HVC_primary_fraction'] + production["HVC (mechanical recycling)"] = get(config["HVC_mechanical_recycling_fraction"], investment_year) * production["HVC"] + production["HVC (chemical recycling)"] = get(config["HVC_chemical_recycling_fraction"], investment_year) * production["HVC"] + + production["HVC"] *= get(config['HVC_primary_fraction'], investment_year) fn = snakemake.output.industrial_production_per_country_tomorrow production.to_csv(fn, float_format='%.2f') diff --git a/scripts/build_industrial_production_per_node.py b/scripts/build_industrial_production_per_node.py index b5361e6b..4ceffee9 100644 --- a/scripts/build_industrial_production_per_node.py +++ b/scripts/build_industrial_production_per_node.py @@ -9,7 +9,11 @@ sector_mapping = { 'Integrated steelworks': 'Iron and steel', 'DRI + Electric arc': 'Iron and steel', 'Ammonia': 'Chemical industry', - 'Basic chemicals (without ammonia)': 'Chemical industry', + 'HVC': 'Chemical industry', + 'HVC (mechanical recycling)': 'Chemical industry', + 'HVC (chemical recycling)': 'Chemical industry', + 'Methanol': 'Chemical industry', + 'Chlorine': 'Chemical industry', 'Other chemicals': 'Chemical industry', 'Pharmaceutical products etc.': 'Chemical industry', 'Cement': 'Cement', @@ -40,12 +44,12 @@ def build_nodal_industrial_production(): countries = keys.country.unique() sectors = industrial_production.columns - + for country, sector in product(countries, sectors): buses = keys.index[keys.country == country] mapping = sector_mapping.get(sector, "population") - + key = keys.loc[buses, mapping] nodal_production.loc[buses, sector] = industrial_production.at[country, sector] * key diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index adfb1d3c..49c82138 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -279,7 +279,7 @@ def chemicals_industry(): df = pd.DataFrame(index=index) - # Basid chemicals + # Basic chemicals sector = "Basic chemicals" @@ -374,52 +374,82 @@ def chemicals_industry(): # putting in ammonia demand for H2 and electricity separately s_emi = idees["emi"][3:57] - s_out = idees["out"][8:9] assert s_emi.index[0] == sector - assert sector in str(s_out.index) - ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) - - # ktNH3/a - ammonia_total = ammonia.loc[ammonia.index.intersection(eu28), str(year)].sum() - - s_out -= ammonia_total + # convert from MtHVC/a to ktHVC/a + s_out = config["HVC_production_today"] * 1e3 # tCO2/t material df.loc["process emission", sector] += ( s_emi["Process emissions"] - config["petrochemical_process_emissions"] * 1e3 - config["NH3_process_emissions"] * 1e3 - ) / s_out.values + ) / s_out # emissions originating from feedstock, could be non-fossil origin # tCO2/t material df.loc["process emission from feedstock", sector] += ( config["petrochemical_process_emissions"] * 1e3 - ) / s_out.values + ) / s_out # convert from ktoe/a to GWh/a sources = ["elec", "biomass", "methane", "hydrogen", "heat", "naphtha"] df.loc[sources, sector] *= toe_to_MWh + # subtract ammonia energy demand (in ktNH3/a) + ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) + ammonia_total = ammonia.loc[ammonia.index.intersection(eu28), str(year)].sum() df.loc["methane", sector] -= ammonia_total * config["MWh_CH4_per_tNH3_SMR"] df.loc["elec", sector] -= ammonia_total * config["MWh_elec_per_tNH3_SMR"] - # MWh/t material - df.loc[sources, sector] = df.loc[sources, sector] / s_out.values + # subtract chlorine demand + chlorine_total = config["chlorine_production_today"] + df.loc["hydrogen", sector] -= chlorine_total * config["MWh_H2_per_tCl"] + df.loc["elec", sector] -= chlorine_total * config["MWh_elec_per_tCl"] - to_rename = {sector: f"{sector} (without ammonia)"} - df.rename(columns=to_rename, inplace=True) + # subtract methanol demand + methanol_total = config["methanol_production_today"] + df.loc["methane", sector] -= methanol_total * config["MWh_CH4_per_tMeOH"] + df.loc["elec", sector] -= methanol_total * config["MWh_elec_per_tMeOH"] + + # MWh/t material + df.loc[sources, sector] = df.loc[sources, sector] / s_out + + df.rename(columns={sector: "HVC"}, inplace=True) + + # HVC mechanical recycling + + sector = "HVC (mechanical recycling)" + df[sector] = 0.0 + df.loc["elec", sector] = config["MWh_elec_per_tHVC_mechanical_recycling"] + + # HVC chemical recycling + + sector = "HVC (chemical recycling)" + df[sector] = 0.0 + df.loc["elec", sector] = config["MWh_elec_per_tHVC_chemical_recycling"] # Ammonia sector = "Ammonia" - df[sector] = 0.0 - df.loc["hydrogen", sector] = config["MWh_H2_per_tNH3_electrolysis"] df.loc["elec", sector] = config["MWh_elec_per_tNH3_electrolysis"] + # Chlorine + + sector = "Chlorine" + df[sector] = 0.0 + df.loc["hydrogen", sector] = config["MWh_H2_per_tCl"] + df.loc["elec", sector] = config["MWh_elec_per_tCl"] + + # Methanol + + sector = "Methanol" + df[sector] = 0.0 + df.loc["methane", sector] = config["MWh_CH4_per_tMeOH"] + df.loc["elec", sector] = config["MWh_elec_per_tMeOH"] + # Other chemicals sector = "Other chemicals" diff --git a/scripts/build_population_layouts.py b/scripts/build_population_layouts.py index 57934fb2..6c229797 100644 --- a/scripts/build_population_layouts.py +++ b/scripts/build_population_layouts.py @@ -90,8 +90,8 @@ if __name__ == '__main__': for key, pop in pop_cells.items(): - ycoords = ('y', cutout.coords['y']) - xcoords = ('x', cutout.coords['x']) + ycoords = ('y', cutout.coords['y'].data) + xcoords = ('x', cutout.coords['x'].data) values = pop.values.reshape(cutout.shape) layout = xr.DataArray(values, [ycoords, xcoords]) diff --git a/scripts/copy_config.py b/scripts/copy_config.py index 2f329dc3..d3408dbc 100644 --- a/scripts/copy_config.py +++ b/scripts/copy_config.py @@ -5,7 +5,8 @@ files = [ "config.yaml", "Snakefile", "scripts/solve_network.py", - "scripts/prepare_sector_network.py" + "scripts/prepare_sector_network.py", + "../pypsa-eur/config.yaml" ] if __name__ == '__main__': diff --git a/scripts/plot_network.py b/scripts/plot_network.py index a328cd03..a78b6551 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -19,9 +19,11 @@ def rename_techs_tyndp(tech): tech = rename_techs(tech) if "heat pump" in tech or "resistive heater" in tech: return "power-to-heat" - elif tech in ["methanation", "hydrogen storage", "helmeth"]: + elif tech in ["H2 Electrolysis", "methanation", "helmeth", "H2 liquefaction"]: return "power-to-gas" - elif tech in ["OCGT", "CHP", "gas boiler"]: + elif tech == "H2": + return "H2 storage" + elif tech in ["OCGT", "CHP", "gas boiler", "H2 Fuel Cell"]: return "gas-to-power/heat" elif "solar" in tech: return "solar" @@ -29,6 +31,8 @@ def rename_techs_tyndp(tech): return "power-to-liquid" elif "offshore wind" in tech: return "offshore wind" + elif "CC" in tech or "sequestration" in tech: + return "CCS" else: return tech @@ -286,13 +290,13 @@ def plot_h2_map(network): l2 = ax.legend( handles, labels, loc="upper left", - bbox_to_anchor=(0.01, 1.01), + bbox_to_anchor=(-0.03, 1.01), labelspacing=1.0, frameon=False, title='Electrolyzer capacity', handler_map=make_handler_map_to_scale_circles_as_in(ax) ) - + ax.add_artist(l2) handles = [] @@ -662,7 +666,8 @@ def plot_series(network, carrier="AC", name="test"): supply = pd.DataFrame(index=n.snapshots) for c in n.iterate_components(n.branch_components): - for i in range(2): + n_port = 4 if c.name=='Link' else 2 + for i in range(n_port): supply = pd.concat((supply, (-1) * c.pnl["p" + str(i)].loc[:, c.df.index[c.df["bus" + str(i)].isin(buses)]].groupby(c.df.carrier, @@ -831,10 +836,11 @@ if __name__ == "__main__": snakemake = mock_snakemake( 'plot_network', simpl='', - clusters=48, - lv=1.0, - sector_opts='Co2L0-168H-T-H-B-I-solar3-dist1', - planning_horizons=2050, + clusters=45, + lv=1.5, + opts='', + sector_opts='Co2L0-168H-T-H-B-I-solar+p3-dist1', + planning_horizons=2030, ) overrides = override_component_attrs(snakemake.input.overrides) diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 86ac462f..8b073b17 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -34,9 +34,11 @@ def rename_techs(label): rename_if_contains_dict = { "water tanks": "hot water storage", "retrofitting": "building retrofitting", - "H2": "hydrogen storage", + # "H2 Electrolysis": "hydrogen storage", + # "H2 Fuel Cell": "hydrogen storage", + # "H2 pipeline": "hydrogen storage", "battery": "battery storage", - "CC": "CC" + # "CC": "CC" } rename = { @@ -88,6 +90,7 @@ preferred_order = pd.Index([ "offshore wind (DC)", "solar PV", "solar thermal", + "solar rooftop", "solar", "building retrofitting", "ground heat pump", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 96f30ae4..9cf9acf0 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -19,7 +19,6 @@ from helper import override_component_attrs import logging logger = logging.getLogger(__name__) - from types import SimpleNamespace spatial = SimpleNamespace() @@ -27,7 +26,7 @@ spatial = SimpleNamespace() def define_spatial(nodes): """ Namespace for spatial - + Parameters ---------- nodes : list-like @@ -38,6 +37,40 @@ def define_spatial(nodes): spatial.nodes = nodes + # biomass + + spatial.biomass = SimpleNamespace() + + if options["biomass_transport"]: + spatial.biomass.nodes = nodes + " solid biomass" + spatial.biomass.locations = nodes + spatial.biomass.industry = nodes + " solid biomass for industry" + spatial.biomass.industry_cc = nodes + " solid biomass for industry CC" + else: + spatial.biomass.nodes = ["EU solid biomass"] + spatial.biomass.locations = ["EU"] + spatial.biomass.industry = ["solid biomass for industry"] + spatial.biomass.industry_cc = ["solid biomass for industry CC"] + + spatial.biomass.df = pd.DataFrame(vars(spatial.biomass), index=nodes) + + # co2 + + spatial.co2 = SimpleNamespace() + + if options["co2_network"]: + spatial.co2.nodes = nodes + " co2 stored" + spatial.co2.locations = nodes + spatial.co2.vents = nodes + " co2 vent" + else: + spatial.co2.nodes = ["co2 stored"] + spatial.co2.locations = ["EU"] + spatial.co2.vents = ["co2 vent"] + + spatial.co2.df = pd.DataFrame(vars(spatial.co2), index=nodes) + + # gas + spatial.gas = SimpleNamespace() if options["gas_network"]: @@ -56,6 +89,10 @@ def define_spatial(nodes): spatial.gas.df = pd.DataFrame(vars(spatial.gas), index=nodes) +from types import SimpleNamespace +spatial = SimpleNamespace() + + def emission_sectors_from_opts(opts): sectors = ["electricity"] @@ -78,6 +115,10 @@ def emission_sectors_from_opts(opts): "domestic navigation", "international navigation" ] + if "A" in opts: + sectors += [ + "agriculture" + ] return sectors @@ -90,6 +131,40 @@ def get(item, investment_year=None): return item +def create_network_topology(n, prefix, connector=" -> "): + """ + Create a network topology like the power transmission network. + + Parameters + ---------- + n : pypsa.Network + prefix : str + connector : str + + Returns + ------- + pd.DataFrame with columns bus0, bus1 and length + """ + + ln_attrs = ["bus0", "bus1", "length"] + lk_attrs = ["bus0", "bus1", "length", "underwater_fraction"] + + candidates = pd.concat([ + n.lines[ln_attrs], + n.links.loc[n.links.carrier == "DC", lk_attrs] + ]).fillna(0) + + positive_order = candidates.bus0 < candidates.bus1 + candidates_p = candidates[positive_order] + swap_buses = {"bus0": "bus1", "bus1": "bus0"} + candidates_n = candidates[~positive_order].rename(columns=swap_buses) + candidates = pd.concat([candidates_p, candidates_n]) + + topo = candidates.groupby(["bus0", "bus1"], as_index=False).mean() + topo.index = topo.apply(lambda c: prefix + c.bus0 + connector + c.bus1, axis=1) + return topo + + def co2_emissions_year(countries, opts, year): """ Calculate CO2 emissions in one specific year (e.g. 1990 or 2018). @@ -111,7 +186,7 @@ def co2_emissions_year(countries, opts, year): co2_emissions = co2_totals.loc[countries, sectors].sum().sum() # convert MtCO2 to GtCO2 - co2_emissions *= 0.001 + co2_emissions *= 0.001 return co2_emissions @@ -138,17 +213,14 @@ def build_carbon_budget(o, fn): #emissions at the beginning of the path (last year available 2018) e_0 = co2_emissions_year(countries, opts, year=2018) - - #emissions in 2019 and 2020 assumed equal to 2018 and substracted - carbon_budget -= 2 * e_0 - + planning_horizons = snakemake.config['scenario']['planning_horizons'] t_0 = planning_horizons[0] if "be" in o: # final year in the path - t_f = t_0 + (2 * carbon_budget / e_0).round(0) + t_f = t_0 + (2 * carbon_budget / e_0).round(0) def beta_decay(t): cdf_term = (t - t_0) / (t_f - t_0) @@ -180,6 +252,53 @@ def add_lifetime_wind_solar(n, costs): n.generators.loc[gen_i, "lifetime"] = costs.at[carrier, 'lifetime'] +def create_network_topology(n, prefix, connector=" -> ", bidirectional=True): + """ + Create a network topology like the power transmission network. + + Parameters + ---------- + n : pypsa.Network + prefix : str + connector : str + bidirectional : bool, default True + True: one link for each connection + False: one link for each connection and direction (back and forth) + + Returns + ------- + pd.DataFrame with columns bus0, bus1 and length + """ + + ln_attrs = ["bus0", "bus1", "length"] + lk_attrs = ["bus0", "bus1", "length", "underwater_fraction"] + + candidates = pd.concat([ + n.lines[ln_attrs], + n.links.loc[n.links.carrier == "DC", lk_attrs] + ]).fillna(0) + + positive_order = candidates.bus0 < candidates.bus1 + candidates_p = candidates[positive_order] + swap_buses = {"bus0": "bus1", "bus1": "bus0"} + candidates_n = candidates[~positive_order].rename(columns=swap_buses) + candidates = pd.concat([candidates_p, candidates_n]) + + def make_index(c): + return prefix + c.bus0 + connector + c.bus1 + + topo = candidates.groupby(["bus0", "bus1"], as_index=False).mean() + topo.index = topo.apply(make_index, axis=1) + + if not bidirectional: + topo_reverse = topo.copy() + topo_reverse.rename(columns=swap_buses, inplace=True) + topo_reverse.index = topo_reverse.apply(make_index, axis=1) + topo = topo.append(topo_reverse) + + return topo + + # TODO merge issue with PyPSA-Eur def update_wind_solar_costs(n, costs): """ @@ -312,6 +431,9 @@ def patch_electricity_network(n): update_wind_solar_costs(n, costs) n.loads["carrier"] = "electricity" n.buses["location"] = n.buses.index + # remove trailing white space of load index until new PyPSA version after v0.18. + n.loads.rename(lambda x: x.strip(), inplace=True) + n.loads_t.p_set.rename(lambda x: x.strip(), axis=1, inplace=True) def add_co2_tracking(n, options): @@ -338,26 +460,26 @@ def add_co2_tracking(n, options): ) # this tracks CO2 stored, e.g. underground - n.add("Bus", - "co2 stored", - location="EU", + n.madd("Bus", + spatial.co2.nodes, + location=spatial.co2.locations, carrier="co2 stored" ) - n.add("Store", - "co2 stored", + n.madd("Store", + spatial.co2.nodes, e_nom_extendable=True, - e_nom_max=options['co2_sequestration_potential'] * 1e6, + e_nom_max=np.inf, capital_cost=options['co2_sequestration_cost'], carrier="co2 stored", - bus="co2 stored" + bus=spatial.co2.nodes ) if options['co2_vent']: - n.add("Link", - "co2 vent", - bus0="co2 stored", + n.madd("Link", + spatial.co2.vents, + bus0=spatial.co2.nodes, bus1="co2 atmosphere", carrier="co2 vent", efficiency=1., @@ -365,6 +487,28 @@ def add_co2_tracking(n, options): ) +def add_co2_network(n, costs): + + logger.info("Adding CO2 network.") + co2_links = create_network_topology(n, "CO2 pipeline ") + + cost_onshore = (1 - co2_links.underwater_fraction) * costs.at['CO2 pipeline', 'fixed'] * co2_links.length + cost_submarine = co2_links.underwater_fraction * costs.at['CO2 submarine pipeline', 'fixed'] * co2_links.length + capital_cost = cost_onshore + cost_submarine + + n.madd("Link", + co2_links.index, + bus0=co2_links.bus0.values + " co2 stored", + bus1=co2_links.bus1.values + " co2 stored", + p_min_pu=-1, + p_nom_extendable=True, + length=co2_links.length.values, + capital_cost=capital_cost.values, + carrier="CO2 pipeline", + lifetime=costs.at['CO2 pipeline', 'lifetime'] + ) + + def add_dac(n, costs): heat_carriers = ["urban central heat", "services urban decentral heat"] @@ -375,10 +519,9 @@ def add_dac(n, costs): efficiency3 = -(costs.at['direct air capture', 'heat-input'] - costs.at['direct air capture', 'compression-heat-output']) n.madd("Link", - locations, - suffix=" DAC", + heat_buses.str.replace(" heat", " DAC"), bus0="co2 atmosphere", - bus1="co2 stored", + bus1=spatial.co2.df.loc[locations, "nodes"].values, bus2=locations.values, bus3=heat_buses, carrier="DAC", @@ -522,6 +665,8 @@ def prepare_data(n): nodal_energy_totals = energy_totals.loc[pop_layout.ct].fillna(0.) nodal_energy_totals.index = pop_layout.index + # district heat share not weighted by population + district_heat_share = nodal_energy_totals["district heat share"].round(2) nodal_energy_totals = nodal_energy_totals.multiply(pop_layout.fraction, axis=0) # copy forward the daily average heat demand into each hour, so it can be multipled by the intraday profile @@ -644,7 +789,7 @@ def prepare_data(n): ) - return nodal_energy_totals, heat_demand, ashp_cop, gshp_cop, solar_thermal, transport, avail_profile, dsm_profile, nodal_transport_data + return nodal_energy_totals, heat_demand, ashp_cop, gshp_cop, solar_thermal, transport, avail_profile, dsm_profile, nodal_transport_data, district_heat_share # TODO checkout PyPSA-Eur script @@ -772,8 +917,9 @@ def insert_electricity_distribution_grid(n, costs): capital_cost=costs.at['electricity distribution grid', 'fixed'] * cost_factor ) - # this catches regular electricity load and "industry electricity" - loads = n.loads.index[n.loads.carrier.str.contains("electricity")] + # this catches regular electricity load and "industry electricity" and + # "agriculture machinery electric" and "agriculture electricity" + loads = n.loads.index[n.loads.carrier.str.contains("electric")] n.loads.loc[loads, "bus"] += " low voltage" bevs = n.links.index[n.links.carrier == "BEV charger"] @@ -814,7 +960,8 @@ def insert_electricity_distribution_grid(n, costs): marginal_cost=n.generators.loc[solar, 'marginal_cost'], capital_cost=costs.at['solar-rooftop', 'fixed'], efficiency=n.generators.loc[solar, 'efficiency'], - p_max_pu=n.generators_t.p_max_pu[solar] + p_max_pu=n.generators_t.p_max_pu[solar], + lifetime=costs.at['solar-rooftop', 'lifetime'] ) n.add("Carrier", "home battery") @@ -862,7 +1009,7 @@ def insert_gas_distribution_costs(n, costs): # TODO options? f_costs = options['gas_distribution_grid_cost_factor'] - + print("Inserting gas distribution grid with investment cost factor of", f_costs) capital_cost = costs.loc['electricity distribution grid']["fixed"] * f_costs @@ -871,7 +1018,7 @@ def insert_gas_distribution_costs(n, costs): gas_b = n.links.index[n.links.carrier.str.contains("gas boiler") & (~n.links.carrier.str.contains("urban central"))] n.links.loc[gas_b, "capital_cost"] += capital_cost - + # micro CHPs mchp = n.links.index[n.links.carrier.str.contains("micro gas")] n.links.loc[mchp, "capital_cost"] += capital_cost @@ -947,7 +1094,7 @@ def add_storage(n, costs): ) # hydrogen stored overground (where not already underground) - h2_capital_cost = costs.at["hydrogen storage tank", "fixed"] + h2_capital_cost = costs.at["hydrogen storage tank incl. compressor", "fixed"] nodes_overground = cavern_nodes.index.symmetric_difference(nodes) n.madd("Store", @@ -982,9 +1129,9 @@ def add_storage(n, costs): p_min_pu=-1, p_nom_extendable=True, length=h2_links.length.values, - capital_cost=costs.at['H2 pipeline', 'fixed'] * h2_links.length.values, + capital_cost=costs.at['H2 (g) pipeline', 'fixed'] * h2_links.length.values, carrier="H2 pipeline", - lifetime=costs.at['H2 pipeline', 'lifetime'] + lifetime=costs.at['H2 (g) pipeline', 'lifetime'] ) if options["gas_network"]: @@ -1120,25 +1267,27 @@ def add_storage(n, costs): if options['methanation']: n.madd("Link", - nodes + " Sabatier", + spatial.nodes, + suffix=" Sabatier", bus0=nodes + " H2", bus1=spatial.gas.nodes, - bus2="co2 stored", + bus2=spatial.co2.nodes, p_nom_extendable=True, carrier="Sabatier", efficiency=costs.at["methanation", "efficiency"], efficiency2=-costs.at["methanation", "efficiency"] * costs.at['gas', 'CO2 intensity'], - capital_cost=costs.at["methanation", "fixed"], + capital_cost=costs.at["methanation", "fixed"] * costs.at["methanation", "efficiency"], # costs given per kW_gas lifetime=costs.at['methanation', 'lifetime'] ) if options['helmeth']: n.madd("Link", - nodes + " helmeth", + spatial.nodes, + suffix=" helmeth", bus0=nodes, bus1=spatial.gas.nodes, - bus2="co2 stored", + bus2=spatial.co2.nodes, carrier="helmeth", p_nom_extendable=True, efficiency=costs.at["helmeth", "efficiency"], @@ -1151,11 +1300,12 @@ def add_storage(n, costs): if options['SMR']: n.madd("Link", - nodes + " SMR CC", + spatial.nodes, + suffix=" SMR CC", bus0=spatial.gas.nodes, bus1=nodes + " H2", bus2="co2 atmosphere", - bus3="co2 stored", + bus3=spatial.co2.nodes, p_nom_extendable=True, carrier="SMR CC", efficiency=costs.at["SMR CC", "efficiency"], @@ -1206,7 +1356,7 @@ def add_land_transport(n, costs): suffix=" EV battery", carrier="Li ion" ) - + p_set = electric_share * (transport[nodes] + cycling_shift(transport[nodes], 1) + cycling_shift(transport[nodes], 2)) / 3 n.madd("Load", @@ -1217,8 +1367,8 @@ def add_land_transport(n, costs): p_set=p_set ) - - p_nom = nodal_transport_data["number cars"] * options.get("bev_charge_rate", 0.011) * electric_share + + p_nom = nodal_transport_data["number cars"] * options.get("bev_charge_rate", 0.011) * electric_share n.madd("Link", nodes, @@ -1250,7 +1400,7 @@ def add_land_transport(n, costs): if electric_share > 0 and options["bev_dsm"]: - e_nom = nodal_transport_data["number cars"] * options.get("bev_energy", 0.05) * options["bev_availability"] * electric_share + e_nom = nodal_transport_data["number cars"] * options.get("bev_energy", 0.05) * options["bev_availability"] * electric_share n.madd("Store", nodes, @@ -1294,8 +1444,8 @@ def add_land_transport(n, costs): co2 = ice_share / ice_efficiency * transport[nodes].sum().sum() / 8760 * costs.at["oil", 'CO2 intensity'] - n.madd("Load", - ["land transport oil emissions"], + n.add("Load", + "land transport oil emissions", bus="co2 atmosphere", carrier="land transport oil emissions", p_set=-co2 @@ -1308,12 +1458,11 @@ def add_heat(n, costs): sectors = ["residential", "services"] - nodes = create_nodes_for_heat_sector() + + nodes, dist_fraction, urban_fraction = create_nodes_for_heat_sector() #NB: must add costs of central heating afterwards (EUR 400 / kWpeak, 50a, 1% FOM from Fraunhofer ISE) - urban_fraction = options['central_fraction'] * pop_layout["urban"] / pop_layout[["urban", "rural"]].sum(axis=1) - # exogenously reduce space heat demand if options["reduce_space_heat_exogenously"]: dE = get(options["reduce_space_heat_exogenously_factor"], investment_year) @@ -1328,7 +1477,7 @@ def add_heat(n, costs): "services urban decentral", "urban central" ] - + for name in heat_systems: name_type = "central" if name == "urban central" else "decentral" @@ -1344,15 +1493,22 @@ def add_heat(n, costs): ## Add heat load for sector in sectors: + # heat demand weighting if "rural" in name: factor = 1 - urban_fraction[nodes[name]] - elif "urban" in name: - factor = urban_fraction[nodes[name]] + elif "urban central" in name: + factor = dist_fraction[nodes[name]] + elif "urban decentral" in name: + factor = urban_fraction[nodes[name]] - \ + dist_fraction[nodes[name]] + else: + raise NotImplementedError(f" {name} not in " f"heat systems: {heat_systems}") + if sector in name: heat_load = heat_demand[[sector + " water",sector + " space"]].groupby(level=1,axis=1).sum()[nodes[name]].multiply(factor) if name == "urban central": - heat_load = heat_demand.groupby(level=1,axis=1).sum()[nodes[name]].multiply(urban_fraction[nodes[name]] * (1 + options['district_heating_loss'])) + heat_load = heat_demand.groupby(level=1,axis=1).sum()[nodes[name]].multiply(factor * (1 + options['district_heating']['district_heating_loss'])) n.madd("Load", nodes[name], @@ -1410,16 +1566,16 @@ def add_heat(n, costs): p_nom_extendable=True ) - + if isinstance(options["tes_tau"], dict): tes_time_constant_days = options["tes_tau"][name_type] else: logger.warning("Deprecated: a future version will require you to specify 'tes_tau' ", "for 'decentral' and 'central' separately.") tes_time_constant_days = options["tes_tau"] if name_type == "decentral" else 180. - + # conversion from EUR/m^3 to EUR/MWh for 40 K diff and 1.17 kWh/m^3/K - capital_cost = costs.at[name_type + ' water tank storage', 'fixed'] / 0.00117 / 40 + capital_cost = costs.at[name_type + ' water tank storage', 'fixed'] / 0.00117 / 40 n.madd("Store", nodes[name] + f" {name} water tanks", @@ -1502,7 +1658,7 @@ def add_heat(n, costs): bus1=nodes[name], bus2=nodes[name] + " urban central heat", bus3="co2 atmosphere", - bus4="co2 stored", + bus4=spatial.co2.df.loc[nodes[name], "nodes"].values, carrier="urban central gas CHP CC", p_nom_extendable=True, capital_cost=costs.at['central gas CHP', 'fixed']*costs.at['central gas CHP', 'efficiency'] + costs.at['biomass CHP capture', 'fixed']*costs.at['gas', 'CO2 intensity'], @@ -1632,48 +1788,61 @@ def create_nodes_for_heat_sector(): # rural are areas with low heating density and individual heating # urban are areas with high heating density # urban can be split into district heating (central) and individual heating (decentral) - + + ct_urban = pop_layout.urban.groupby(pop_layout.ct).sum() + # distribution of urban population within a country + pop_layout["urban_ct_fraction"] = pop_layout.urban / pop_layout.ct.map(ct_urban.get) + sectors = ["residential", "services"] - + nodes = {} + urban_fraction = pop_layout.urban / pop_layout[["rural", "urban"]].sum(axis=1) + for sector in sectors: nodes[sector + " rural"] = pop_layout.index + nodes[sector + " urban decentral"] = pop_layout.index - if options["central"]: - # TODO: this looks hardcoded, move to config - urban_decentral_ct = pd.Index(["ES", "GR", "PT", "IT", "BG"]) - nodes[sector + " urban decentral"] = pop_layout.index[pop_layout.ct.isin(urban_decentral_ct)] - else: - nodes[sector + " urban decentral"] = pop_layout.index - - # for central nodes, residential and services are aggregated - nodes["urban central"] = pop_layout.index.symmetric_difference(nodes["residential urban decentral"]) - - return nodes + # maximum potential of urban demand covered by district heating + central_fraction = options['district_heating']["potential"] + # district heating share at each node + dist_fraction_node = district_heat_share * pop_layout["urban_ct_fraction"] / pop_layout["fraction"] + nodes["urban central"] = dist_fraction_node.index + # if district heating share larger than urban fraction -> set urban + # fraction to district heating share + urban_fraction = pd.concat([urban_fraction, dist_fraction_node], + axis=1).max(axis=1) + # difference of max potential and today's share of district heating + diff = (urban_fraction * central_fraction) - dist_fraction_node + progress = get(options["district_heating"]["progress"], investment_year) + dist_fraction_node += diff * progress + print( + "The current district heating share compared to the maximum", + f"possible is increased by a progress factor of\n{progress}", + f"resulting in a district heating share of\n{dist_fraction_node}" + ) + + return nodes, dist_fraction_node, urban_fraction def add_biomass(n, costs): print("adding biomass") - # biomass distributed at country level - i.e. transport within country allowed - countries = n.buses.country.dropna().unique() - biomass_potentials = pd.read_csv(snakemake.input.biomass_potentials, index_col=0) - # potential per node distributed within country by population - biogas_pot = (biomass_potentials.loc[pop_layout.ct] - .set_index(pop_layout.index) - .mul(pop_layout.fraction, axis="index") - .rename(index=lambda x: x + " biogas") - )["biogas"] - # need to aggregate potentials if gas not nodally resolved - if not options["gas_network"]: - biogas_pot = biogas_pot.sum() + if options["gas_network"]: + biogas_potentials_spatial = biomass_potentials["biogas"].rename(index=lambda x: x + " biogas") + else: + biogas_potentials_spatial = biomass_potentials["biogas"].sum() + + if options["biomass_transport"]: + solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].rename(index=lambda x: x + " solid biomass") + else: + solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].sum() + n.add("Carrier", "biogas") - n.add("Carrier", "solid biomass") n.madd("Bus", @@ -1682,9 +1851,9 @@ def add_biomass(n, costs): carrier="biogas" ) - n.add("Bus", - "EU solid biomass", - location="EU", + n.madd("Bus", + spatial.biomass.nodes, + location=spatial.biomass.locations, carrier="solid biomass" ) @@ -1692,18 +1861,18 @@ def add_biomass(n, costs): spatial.gas.biogas, bus=spatial.gas.biogas, carrier="biogas", - e_nom=biogas_pot, + e_nom=biogas_potentials_spatial, marginal_cost=costs.at['biogas', 'fuel'], - e_initial=biogas_pot + e_initial=biogas_potentials_spatial ) - n.add("Store", - "EU solid biomass", - bus="EU solid biomass", + n.madd("Store", + spatial.biomass.nodes, + bus=spatial.biomass.nodes, carrier="solid biomass", - e_nom=biomass_potentials.loc[countries, "solid biomass"].sum(), + e_nom=solid_biomass_potentials_spatial, marginal_cost=costs.at['solid biomass', 'fuel'], - e_initial=biomass_potentials.loc[countries, "solid biomass"].sum() + e_initial=solid_biomass_potentials_spatial ) n.madd("Link", @@ -1718,6 +1887,32 @@ def add_biomass(n, costs): p_nom_extendable=True ) + if options["biomass_transport"]: + + transport_costs = pd.read_csv( + snakemake.input.biomass_transport_costs, + index_col=0, + squeeze=True + ) + + # add biomass transport + biomass_transport = create_network_topology(n, "biomass transport ", bidirectional=False) + + # costs + bus0_costs = biomass_transport.bus0.apply(lambda x: transport_costs[x[:2]]) + bus1_costs = biomass_transport.bus1.apply(lambda x: transport_costs[x[:2]]) + biomass_transport["costs"] = pd.concat([bus0_costs, bus1_costs], axis=1).mean(axis=1) + + n.madd("Link", + biomass_transport.index, + bus0=biomass_transport.bus0 + " solid biomass", + bus1=biomass_transport.bus1 + " solid biomass", + p_nom_extendable=True, + length=biomass_transport.length.values, + marginal_cost=biomass_transport.costs * biomass_transport.length.values, + capital_cost=1, + carrier="solid biomass transport" + ) #AC buses with district heating urban_central = n.buses.index[n.buses.carrier == "urban central heat"] @@ -1728,7 +1923,7 @@ def add_biomass(n, costs): n.madd("Link", urban_central + " urban central solid biomass CHP", - bus0="EU solid biomass", + bus0=spatial.biomass.df.loc[urban_central, "nodes"].values, bus1=urban_central, bus2=urban_central + " urban central heat", carrier="urban central solid biomass CHP", @@ -1742,11 +1937,11 @@ def add_biomass(n, costs): n.madd("Link", urban_central + " urban central solid biomass CHP CC", - bus0="EU solid biomass", + bus0=spatial.biomass.df.loc[urban_central, "nodes"].values, bus1=urban_central, bus2=urban_central + " urban central heat", bus3="co2 atmosphere", - bus4="co2 stored", + bus4=spatial.co2.df.loc[urban_central, "nodes"].values, carrier="urban central solid biomass CHP CC", p_nom_extendable=True, capital_cost=costs.at[key, 'fixed'] * costs.at[key, 'efficiency'] + costs.at['biomass CHP capture', 'fixed'] * costs.at['solid biomass', 'CO2 intensity'], @@ -1776,34 +1971,39 @@ def add_industry(n, costs): solid_biomass_by_country = industrial_demand["solid biomass"].groupby(pop_layout.ct).sum() - n.add("Bus", - "solid biomass for industry", - location="EU", + n.madd("Bus", + spatial.biomass.industry, + location=spatial.biomass.locations, carrier="solid biomass for industry" ) - n.add("Load", - "solid biomass for industry", - bus="solid biomass for industry", + if options["biomass_transport"]: + p_set = industrial_demand.loc[spatial.biomass.locations, "solid biomass"].rename(index=lambda x: x + " solid biomass for industry") / 8760 + else: + p_set = industrial_demand["solid biomass"].sum() / 8760 + + n.madd("Load", + spatial.biomass.industry, + bus=spatial.biomass.industry, carrier="solid biomass for industry", - p_set=solid_biomass_by_country.sum() / 8760 + p_set=p_set ) - n.add("Link", - "solid biomass for industry", - bus0="EU solid biomass", - bus1="solid biomass for industry", + n.madd("Link", + spatial.biomass.industry, + bus0=spatial.biomass.nodes, + bus1=spatial.biomass.industry, carrier="solid biomass for industry", p_nom_extendable=True, efficiency=1. ) - n.add("Link", - "solid biomass for industry CC", - bus0="EU solid biomass", - bus1="solid biomass for industry", + n.madd("Link", + spatial.biomass.industry_cc, + bus0=spatial.biomass.nodes, + bus1=spatial.biomass.industry, bus2="co2 atmosphere", - bus3="co2 stored", + bus3=spatial.co2.nodes, carrier="solid biomass for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "fixed"] * costs.at['solid biomass', 'CO2 intensity'], @@ -1841,7 +2041,7 @@ def add_industry(n, costs): bus0=spatial.gas.nodes, bus1=spatial.gas.industry, bus2="co2 atmosphere", - bus3="co2 stored", + bus3=spatial.co2.nodes, carrier="gas for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "fixed"] * costs.at['gas', 'CO2 intensity'], @@ -1859,18 +2059,66 @@ def add_industry(n, costs): p_set=industrial_demand.loc[nodes, "hydrogen"] / 8760 ) + if options["shipping_hydrogen_liquefaction"]: + + n.madd("Bus", + nodes, + suffix=" H2 liquid", + carrier="H2 liquid", + location=nodes + ) + + n.madd("Link", + nodes + " H2 liquefaction", + bus0=nodes + " H2", + bus1=nodes + " H2 liquid", + carrier="H2 liquefaction", + efficiency=costs.at["H2 liquefaction", 'efficiency'], + capital_cost=costs.at["H2 liquefaction", 'fixed'], + p_nom_extendable=True, + lifetime=costs.at['H2 liquefaction', 'lifetime'] + ) + + shipping_bus = nodes + " H2 liquid" + else: + shipping_bus = nodes + " H2" + all_navigation = ["total international navigation", "total domestic navigation"] efficiency = options['shipping_average_efficiency'] / costs.at["fuel cell", "efficiency"] - p_set = nodal_energy_totals.loc[nodes, all_navigation].sum(axis=1) * 1e6 * efficiency / 8760 + shipping_hydrogen_share = get(options['shipping_hydrogen_share'], investment_year) + p_set = shipping_hydrogen_share * nodal_energy_totals.loc[nodes, all_navigation].sum(axis=1) * 1e6 * efficiency / 8760 n.madd("Load", nodes, suffix=" H2 for shipping", - bus=nodes + " H2", + bus=shipping_bus, carrier="H2 for shipping", p_set=p_set ) + if shipping_hydrogen_share < 1: + + shipping_oil_share = 1 - shipping_hydrogen_share + + p_set = shipping_oil_share * nodal_energy_totals.loc[nodes, all_navigation].sum(axis=1) * 1e6 / 8760. + + n.madd("Load", + nodes, + suffix=" shipping oil", + bus="EU oil", + carrier="shipping oil", + p_set=p_set + ) + + co2 = shipping_oil_share * nodal_energy_totals.loc[nodes, all_navigation].sum().sum() * 1e6 / 8760 * costs.at["oil", "CO2 intensity"] + + n.add("Load", + "shipping oil emissions", + bus="co2 atmosphere", + carrier="shipping oil emissions", + p_set=-co2 + ) + if "EU oil" not in n.buses.index: n.add("Bus", @@ -1880,7 +2128,7 @@ def add_industry(n, costs): ) if "EU oil Store" not in n.stores.index: - + #could correct to e.g. 0.001 EUR/kWh * annuity and O&M n.add("Store", "EU oil Store", @@ -1902,7 +2150,7 @@ def add_industry(n, costs): if options["oil_boilers"]: - nodes_heat = create_nodes_for_heat_sector() + nodes_heat = create_nodes_for_heat_sector()[0] for name in ["residential rural", "services rural", "residential urban decentral", "services urban decentral"]: @@ -1923,7 +2171,7 @@ def add_industry(n, costs): nodes + " Fischer-Tropsch", bus0=nodes + " H2", bus1="EU oil", - bus2="co2 stored", + bus2=spatial.co2.nodes, carrier="Fischer-Tropsch", efficiency=costs.at["Fischer-Tropsch", 'efficiency'], capital_cost=costs.at["Fischer-Tropsch", 'fixed'], @@ -2012,11 +2260,12 @@ def add_industry(n, costs): ) #assume enough local waste heat for CC - n.add("Link", - "process emissions CC", + n.madd("Link", + spatial.co2.locations, + suffix=" process emissions CC", bus0="process emissions", bus1="co2 atmosphere", - bus2="co2 stored", + bus2=spatial.co2.nodes, carrier="process emissions CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "fixed"], @@ -2046,8 +2295,73 @@ def add_waste_heat(n): n.links.loc[urban_central + " H2 Fuel Cell", "efficiency2"] = 0.95 - n.links.loc[urban_central + " H2 Fuel Cell", "efficiency"] +def add_agriculture(n, costs): + + logger.info('Add agriculture, forestry and fishing sector.') + + nodes = pop_layout.index + + # electricity + + n.madd("Load", + nodes, + suffix=" agriculture electricity", + bus=nodes, + carrier='agriculture electricity', + p_set=nodal_energy_totals.loc[nodes, "total agriculture electricity"] * 1e6 / 8760 + ) + + # heat + + n.madd("Load", + nodes, + suffix=" agriculture heat", + bus=nodes + " services rural heat", + carrier="agriculture heat", + p_set=nodal_energy_totals.loc[nodes, "total agriculture heat"] * 1e6 / 8760 + ) + + # machinery + + electric_share = get(options["agriculture_machinery_electric_share"], investment_year) + assert electric_share <= 1. + ice_share = 1 - electric_share + + machinery_nodal_energy = nodal_energy_totals.loc[nodes, "total agriculture machinery"] + + if electric_share > 0: + + efficiency_gain = options["agriculture_machinery_fuel_efficiency"] / options["agriculture_machinery_electric_efficiency"] + + n.madd("Load", + nodes, + suffix=" agriculture machinery electric", + bus=nodes, + carrier="agriculture machinery electric", + p_set=electric_share / efficiency_gain * machinery_nodal_energy * 1e6 / 8760, + ) + + if ice_share > 0: + + n.add("Load", + "agriculture machinery oil", + bus="EU oil", + carrier="agriculture machinery oil", + p_set=ice_share * machinery_nodal_energy.sum() * 1e6 / 8760 + ) + + co2 = ice_share * machinery_nodal_energy.sum() * 1e6 / 8760 * costs.at["oil", 'CO2 intensity'] + + n.add("Load", + "agriculture machinery oil emissions", + bus="co2 atmosphere", + carrier="agriculture machinery oil emissions", + p_set=-co2 + ) + + def decentral(n): - """Removes the electricity transmission system.""" + """Removes the electricity transmission system.""" n.lines.drop(n.lines.index, inplace=True) n.links.drop(n.links.index[n.links.carrier.isin(["DC", "B2B"])], inplace=True) @@ -2070,14 +2384,19 @@ def maybe_adjust_costs_and_potentials(n, opts): suptechs = map(lambda c: c.split("-", 2)[0], carrier_list) if oo[0].startswith(tuple(suptechs)): carrier = oo[0] - attr_lookup = {"p": "p_nom_max", "c": "capital_cost"} + attr_lookup = {"p": "p_nom_max", "e": "e_nom_max", "c": "capital_cost"} attr = attr_lookup[oo[1][0]] factor = float(oo[1][1:]) #beware if factor is 0 and p_nom_max is np.inf, 0*np.inf is nan if carrier == "AC": # lines do not have carrier n.lines[attr] *= factor else: - comps = {"Generator", "Link", "StorageUnit"} if attr == 'p_nom_max' else {"Generator", "Link", "StorageUnit", "Store"} + if attr == 'p_nom_max': + comps = {"Generator", "Link", "StorageUnit"} + elif attr == 'e_nom_max': + comps = {"Store"} + else: + comps = {"Generator", "Link", "StorageUnit", "Store"} for c in n.iterate_components(comps): if carrier=='solar': sel = c.df.carrier.str.contains(carrier) & ~c.df.carrier.str.contains("solar rooftop") @@ -2094,17 +2413,18 @@ def limit_individual_line_extension(n, maxext): hvdc = n.links.index[n.links.carrier == 'DC'] n.links.loc[hvdc, 'p_nom_max'] = n.links.loc[hvdc, 'p_nom'] + maxext - +#%% if __name__ == "__main__": if 'snakemake' not in globals(): from helper import mock_snakemake snakemake = mock_snakemake( 'prepare_sector_network', simpl='', - clusters=48, + opts="", + clusters="37", lv=1.0, sector_opts='Co2L0-168H-T-H-B-I-solar3-dist1', - planning_horizons=2020, + planning_horizons="2020", ) logging.basicConfig(level=snakemake.config['logging_level']) @@ -2129,8 +2449,10 @@ if __name__ == "__main__": patch_electricity_network(n) + define_spatial(pop_layout.index) + if snakemake.config["foresight"] == 'myopic': - + add_lifetime_wind_solar(n, costs) conventional = snakemake.config['existing_capacities']['conventional_carriers'] @@ -2152,11 +2474,13 @@ if __name__ == "__main__": if o[:4] == "dist": options['electricity_distribution_grid'] = True options['electricity_distribution_grid_cost_factor'] = float(o[4:].replace("p", ".").replace("m", "-")) + if o == "biomasstransport": + options["biomass_transport"] = True - nodal_energy_totals, heat_demand, ashp_cop, gshp_cop, solar_thermal, transport, avail_profile, dsm_profile, nodal_transport_data = prepare_data(n) + nodal_energy_totals, heat_demand, ashp_cop, gshp_cop, solar_thermal, transport, avail_profile, dsm_profile, nodal_transport_data, district_heat_share = prepare_data(n) if "nodistrict" in opts: - options["central"] = False + options["district_heating"]["progress"] = 0.0 if "T" in opts: add_land_transport(n, costs) @@ -2173,6 +2497,9 @@ if __name__ == "__main__": if "I" in opts and "H" in opts: add_waste_heat(n) + if "A" in opts: # requires H and I + add_agriculture(n, costs) + if options['dac']: add_dac(n, costs) @@ -2182,6 +2509,9 @@ if __name__ == "__main__": if "noH2network" in opts: remove_h2_network(n) + if options["co2_network"]: + add_co2_network(n, costs) + for o in opts: m = re.match(r'^\d+h$', o, re.IGNORECASE) if m is not None: diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 632d916a..25666caf 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -3,6 +3,7 @@ import pypsa import numpy as np +import pandas as pd from pypsa.linopt import get_var, linexpr, define_constraints @@ -19,12 +20,47 @@ pypsa.pf.logger.setLevel(logging.WARNING) def add_land_use_constraint(n): - #warning: this will miss existing offwind which is not classed AC-DC and has carrier 'offwind' - for carrier in ['solar', 'onwind', 'offwind-ac', 'offwind-dc']: - existing = n.generators.loc[n.generators.carrier == carrier, "p_nom"].groupby(n.generators.bus.map(n.buses.location)).sum() - existing.index += " " + carrier + "-" + snakemake.wildcards.planning_horizons - n.generators.loc[existing.index, "p_nom_max"] -= existing + if 'm' in snakemake.wildcards.clusters: + _add_land_use_constraint_m(n) + else: + _add_land_use_constraint(n) + +def _add_land_use_constraint(n): + #warning: this will miss existing offwind which is not classed AC-DC and has carrier 'offwind' + + for carrier in ['solar', 'onwind', 'offwind-ac', 'offwind-dc']: + existing = n.generators.loc[n.generators.carrier==carrier,"p_nom"].groupby(n.generators.bus.map(n.buses.location)).sum() + existing.index += " " + carrier + "-" + snakemake.wildcards.planning_horizons + n.generators.loc[existing.index,"p_nom_max"] -= existing + + n.generators.p_nom_max.clip(lower=0, inplace=True) + + +def _add_land_use_constraint_m(n): + # if generators clustering is lower than network clustering, land_use accounting is at generators clusters + + planning_horizons = snakemake.config["scenario"]["planning_horizons"] + grouping_years = snakemake.config["existing_capacities"]["grouping_years"] + current_horizon = snakemake.wildcards.planning_horizons + + for carrier in ['solar', 'onwind', 'offwind-ac', 'offwind-dc']: + + existing = n.generators.loc[n.generators.carrier==carrier,"p_nom"] + ind = list(set([i.split(sep=" ")[0] + ' ' + i.split(sep=" ")[1] for i in existing.index])) + + previous_years = [ + str(y) for y in + planning_horizons + grouping_years + if y < int(snakemake.wildcards.planning_horizons) + ] + + for p_year in previous_years: + ind2 = [i for i in ind if i + " " + carrier + "-" + p_year in existing.index] + sel_current = [i + " " + carrier + "-" + current_horizon for i in ind2] + sel_p_year = [i + " " + carrier + "-" + p_year for i in ind2] + n.generators.loc[sel_current, "p_nom_max"] -= existing.loc[sel_p_year].rename(lambda x: x[:-4] + current_horizon) + n.generators.p_nom_max.clip(lower=0, inplace=True) @@ -150,7 +186,6 @@ def add_chp_constraints(n): define_constraints(n, lhs, "<=", 0, 'chplink', 'backpressure') - def add_pipe_retrofit_constraint(n): """Add constraint for retrofitting existing CH4 pipelines to H2 pipelines.""" @@ -173,10 +208,27 @@ def add_pipe_retrofit_constraint(n): define_constraints(n, lhs, "=", pipe_capacity, 'Link', 'pipe_retrofit') +def add_co2_sequestration_limit(n, sns): + + co2_stores = n.stores.loc[n.stores.carrier=='co2 stored'].index + + if co2_stores.empty or ('Store', 'e') not in n.variables.index: + return + + vars_final_co2_stored = get_var(n, 'Store', 'e').loc[sns[-1], co2_stores] + + lhs = linexpr((1, vars_final_co2_stored)).sum() + rhs = n.config["sector"].get("co2_sequestration_potential", 200) * 1e6 + + name = 'co2_sequestration_limit' + define_constraints(n, lhs, "<=", rhs, 'GlobalConstraint', + 'mu', axes=pd.Index([name]), spec=name) + + def extra_functionality(n, snapshots): - add_chp_constraints(n) add_battery_constraints(n) add_pipe_retrofit_constraint(n) + add_co2_sequestration_limit(n, snapshots) def solve_network(n, config, opts='', **kwargs):