diff --git a/.gitignore b/.gitignore index 894a44c..0bf27b2 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# IDE +.vscode \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7152b80 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md LICENSE \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..24be7ff --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +twine = "*" + +[packages] + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..71f255a --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,128 @@ +{ + "_meta": { + "hash": { + "sha256": "3f58b0bde34362d9df3fcc04995cff87795f870d11c515897605ab725801e6d5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "bleach": { + "hashes": [ + "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", + "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" + ], + "version": "==3.1.0" + }, + "certifi": { + "hashes": [ + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + ], + "version": "==2019.9.11" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "docutils": { + "hashes": [ + "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", + "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", + "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" + ], + "version": "==0.15.2" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "pkginfo": { + "hashes": [ + "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", + "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + ], + "version": "==1.5.0.1" + }, + "pygments": { + "hashes": [ + "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", + "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + ], + "version": "==2.4.2" + }, + "readme-renderer": { + "hashes": [ + "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", + "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" + ], + "version": "==24.0" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "version": "==2.22.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "six": { + "hashes": [ + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + }, + "tqdm": { + "hashes": [ + "sha256:9de4722323451eb7818deb0161d9d5523465353a6707a9f500d97ee42919b902", + "sha256:c1d677f3a85fa291b34bdf8f770f877119b9754b32673699653556f85e2c2f13" + ], + "version": "==4.38.0" + }, + "twine": { + "hashes": [ + "sha256:5319dd3e02ac73fcddcd94f035b9631589ab5d23e1f4699d57365199d85261e1", + "sha256:9fe7091715c7576df166df8ef6654e61bada39571783f2fd415bdcba867c6993" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" + ], + "version": "==1.25.7" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + } + } +} diff --git a/README.md b/README.md index 898ccb2..22162c5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ -# transport-co2 -Library to calculate CO2 (equivalent) emissions for a given transport trip. +# Transport CO2 + +Calculate CO2 (equivalent) emissions for a given transport trip and provide a simple interpretation of the result. + +## Goals + +This project is intended to be used to help raise awareness about the cost of transportation choices, so people can make informed decisions. + +## Initial focus + +We are initially focused on ground (road and rail) transport, as it is the most significant source on transport greenhouse gas emissions. + +## API Design + +This library intends to provide: + +- a statistical estimate of greenhouse gase emissions given information about a trip (origin/destination, mode, and/or distance) +- a simple interpretation of the statistical emissions estimate, in terms such as "high" or "low" + +## Research + +The carbon estimates produced by this model may be based on the following resources. A full description of the model, including data sources, will be provided as the library takes shape. + +- European Environment Agency [CO2 emissions from passenger transport](https://www.eea.europa.eu/media/infographics/co2-emissions-from-passenger-transport/view) + +Further improvements to the model may come from other sources, such as the following. + +- IPCC AR5 [Chapter 8 - Transport](https://www.ipcc.ch/site/assets/uploads/2018/02/ipcc_wg3_ar5_chapter8.pdf) +- Wikipedia: [Environmental impact of transport](https://en.wikipedia.org/wiki/Environmental_impact_of_transport) + +## Attribution + +Initial package structure forked from [navdeep-G/setup.py](https://github.com/navdeep-G/setup.py). + +Friendly nod to [jamiebull1/transport-carbon](https://github.com/jamiebull1/transport-carbon). diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0ba7aaf --- /dev/null +++ b/setup.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Note: To use the 'upload' functionality of this file, you must: +# $ pipenv install twine --dev + +import io +import os +import sys +from shutil import rmtree + +from setuptools import find_packages, setup, Command + +# Package meta-data. +NAME = "transport-co2" +DESCRIPTION = ( + "Library to calculate CO2 (equivalent) emissions for a given transport trip." +) +URL = "https://github.com/maasglobal/transport-co2" +EMAIL = "brylie.oxley@maas.global, markus.schepke@maas.global" +AUTHOR = "Brylie Christopher Oxley, Markus Schepke" +REQUIRES_PYTHON = ">=3.6.0" +VERSION = "0.1.0" + +# What packages are required for this module to be executed? +REQUIRED = [] + +# What packages are optional? +EXTRAS = {} + +# The rest you shouldn't have to touch too much :) +# ------------------------------------------------ +# Except, perhaps the License and Trove Classifiers! +# If you do change the License, remember to change the Trove Classifier for that! + +here = os.path.abspath(os.path.dirname(__file__)) + +# Import the README and use it as the long-description. +# Note: this will only work if 'README.md' is present in your MANIFEST.in file! +try: + with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f: + long_description = "\n" + f.read() +except FileNotFoundError: + long_description = DESCRIPTION + +# Load the package's __version__.py module as a dictionary. +about = {} +if not VERSION: + project_slug = NAME.lower().replace("-", "_").replace(" ", "_") + with open(os.path.join(here, project_slug, "__version__.py")) as f: + exec(f.read(), about) +else: + about["__version__"] = VERSION + + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = "Build and publish the package." + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print("\033[1m{0}\033[0m".format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status("Removing previous builds…") + rmtree(os.path.join(here, "dist")) + except OSError: + pass + + self.status("Building Source and Wheel (universal) distribution…") + os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable)) + + self.status("Uploading the package to PyPI via Twine…") + os.system("twine upload dist/*") + + self.status("Pushing git tags…") + os.system("git tag v{0}".format(about["__version__"])) + os.system("git push --tags") + + sys.exit() + + +# Where the magic happens: +setup( + name=NAME, + version=about["__version__"], + description=DESCRIPTION, + long_description=long_description, + long_description_content_type="text/markdown", + author=AUTHOR, + author_email=EMAIL, + python_requires=REQUIRES_PYTHON, + url=URL, + packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), + # If your package is a single module, use this instead of 'packages': + # py_modules=['mypackage'], + # entry_points={ + # 'console_scripts': ['mycli=mymodule:cli'], + # }, + install_requires=REQUIRED, + extras_require=EXTRAS, + include_package_data=True, + license="MIT", + classifiers=[ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + ], + # $ setup.py publish support. + cmdclass={"upload": UploadCommand}, +) diff --git a/transport_co2/__init__.py b/transport_co2/__init__.py new file mode 100644 index 0000000..c29704f --- /dev/null +++ b/transport_co2/__init__.py @@ -0,0 +1,2 @@ +from .model import Mode +from .estimator import estimate_co2 diff --git a/transport_co2/estimator.py b/transport_co2/estimator.py new file mode 100644 index 0000000..0e63c70 --- /dev/null +++ b/transport_co2/estimator.py @@ -0,0 +1,20 @@ +from typing import Optional, Union +from .model import Mode + + +def estimate_co2( + mode: Union[str, Mode], distance_in_km: float, occupancy: Optional[float] = None +) -> float: + """ + Estimate CO2 usage for transport mode based on KM and optional vehicle occupancy. + + Keyword arguments: + mode -- vehicle mode (limited to allowed values) + distance_in_km -- distance for the trip in kilometers + occupancy -- optional vehicle occupancy (uses average occupancy if falsey) + """ + if isinstance(mode, str): + mode = Mode[mode.upper()] + + return mode.estimate_co2(distance_in_km, occupancy) + diff --git a/transport_co2/model.py b/transport_co2/model.py new file mode 100644 index 0000000..84bd185 --- /dev/null +++ b/transport_co2/model.py @@ -0,0 +1,40 @@ +from enum import Enum +from typing import Optional + + +class Mode(Enum): + """ + Data structure containing grams of CO2/vehicle KM for several modes of transport + along with average occupancy for each mode. + + Source data: + CO2 emissions from passenger transport + from European Environment Agency + https://www.eea.europa.eu/media/infographics/co2-emissions-from-passenger-transport/view + """ + + def __init__(self, co2_per_vehicle_km: float, average_occupancy: float): + self.co2_per_vehicle_km = co2_per_vehicle_km + self.average_occupancy = average_occupancy + + LIGHT_RAIL = (2184, 156) + SMALL_CAR = (168, 1.5) + LARGE_CAR = (220, 1.5) + SCOOTER = (86.4, 1.2) + BUS = (863, 12.7) + + def estimate_co2( + self, distance_in_km: float, occupancy: Optional[float] = None + ) -> float: + """ + Estimate CO2 usage for transport mode based on KM and optional vehicle occupancy. + + Keyword arguments: + distance_in_km -- distance for the trip in kilometers + occupancy -- optional vehicle occupancy (uses average occupancy if falsey) + """ + # occupancy should be above zero + occupancy = occupancy or self.average_occupancy + + return self.co2_per_vehicle_km * distance_in_km / occupancy +