python-aqi 0.5.1__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_aqi-1.0.0/.gitignore +15 -0
- python_aqi-1.0.0/LICENSE +12 -0
- python_aqi-1.0.0/PKG-INFO +291 -0
- python_aqi-1.0.0/README.rst +267 -0
- python_aqi-1.0.0/aqi/__init__.py +248 -0
- python_aqi-1.0.0/aqi/algos/__init__.py +62 -0
- python_aqi-1.0.0/aqi/algos/base.py +242 -0
- python_aqi-1.0.0/aqi/algos/epa.py +207 -0
- python_aqi-1.0.0/aqi/algos/mep.py +128 -0
- python_aqi-1.0.0/aqi/constants.py +23 -0
- python_aqi-1.0.0/aqi/exceptions.py +32 -0
- python_aqi-1.0.0/docs/Makefile +153 -0
- python_aqi-1.0.0/docs/_ext/aqi_compare.py +184 -0
- python_aqi-1.0.0/docs/_ext/aqi_tables.py +80 -0
- python_aqi-1.0.0/docs/_static/aqi_compare.css +14 -0
- python_aqi-1.0.0/docs/_static/aqi_compare.js +159 -0
- python_aqi-1.0.0/docs/_static/uPlot.LICENSE +21 -0
- python_aqi-1.0.0/docs/_static/uPlot.iife.min.js +2 -0
- python_aqi-1.0.0/docs/_static/uPlot.min.css +1 -0
- python_aqi-1.0.0/docs/api.rst +31 -0
- python_aqi-1.0.0/docs/changelog.rst +81 -0
- python_aqi-1.0.0/docs/compare.rst +20 -0
- python_aqi-1.0.0/docs/concepts.rst +96 -0
- python_aqi-1.0.0/docs/conf.py +249 -0
- python_aqi-1.0.0/docs/index.rst +25 -0
- python_aqi-1.0.0/docs/standards/hj633-2026-table.rst +65 -0
- python_aqi-1.0.0/docs/standards/index.rst +195 -0
- python_aqi-1.0.0/docs/versions.rst +196 -0
- python_aqi-1.0.0/pyproject.toml +61 -0
- python_aqi-1.0.0/tests/__init__.py +0 -0
- python_aqi-1.0.0/tests/data/generate_legacy.py +49 -0
- python_aqi-1.0.0/tests/data/legacy_0_6_1.json +1 -0
- python_aqi-1.0.0/tests/test_algos.py +18 -0
- python_aqi-1.0.0/tests/test_aqi.py +29 -0
- python_aqi-1.0.0/tests/test_base.py +20 -0
- python_aqi-1.0.0/tests/test_docs_compare.py +64 -0
- python_aqi-1.0.0/tests/test_docs_tables.py +39 -0
- python_aqi-1.0.0/tests/test_epa.py +63 -0
- python_aqi-1.0.0/tests/test_epa_versions.py +214 -0
- python_aqi-1.0.0/tests/test_legacy.py +51 -0
- python_aqi-1.0.0/tests/test_mep_versions.py +248 -0
- python_aqi-1.0.0/tests/test_out_of_range.py +198 -0
- python_aqi-1.0.0/tests/test_versions.py +178 -0
- home/hr/.virtualenvs/python-aqi/bin/aqi +0 -10
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/__init__.py +0 -107
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/__init__.pyc +0 -0
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/__init__.py +0 -38
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/__init__.pyc +0 -0
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/base.py +0 -85
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/base.pyc +0 -0
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/epa.py +0 -106
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/epa.pyc +0 -0
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/mep.py +0 -137
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/mep.pyc +0 -0
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/constants.py +0 -18
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/constants.pyc +0 -0
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/PKG-INFO +0 -154
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/SOURCES.txt +0 -19
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/dependency_links.txt +0 -1
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/entry_points.txt +0 -3
- home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/top_level.txt +0 -1
python_aqi-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (c) 2014, Stefan "hr" Berder
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
7
|
+
|
|
8
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
9
|
+
|
|
10
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
11
|
+
|
|
12
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: python-aqi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A library to convert between AQI value and pollutant concentration (µg/m³ or ppm)
|
|
5
|
+
Project-URL: Homepage, https://github.com/hrbonz/python-aqi
|
|
6
|
+
Project-URL: Documentation, https://python-aqi.readthedocs.io/
|
|
7
|
+
Author-email: "Stefan \"hr\" Berder" <hr@bonz.org>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: EPA,MEP,air quality,pm2.5
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/x-rst
|
|
24
|
+
|
|
25
|
+
###########################
|
|
26
|
+
python-aqi - AQI conversion
|
|
27
|
+
###########################
|
|
28
|
+
|
|
29
|
+
A library to convert between AQI value and pollutant concentration
|
|
30
|
+
(µg/m³ or ppm) using the following algorithms:
|
|
31
|
+
|
|
32
|
+
* United States Environmental Protection Agency (EPA)
|
|
33
|
+
* China Ministry of Environmental Protection (MEP)
|
|
34
|
+
|
|
35
|
+
The versions of each standard are kept, see `the documentation
|
|
36
|
+
<https://python-aqi.readthedocs.io/>`_ for how an AQI is computed, the
|
|
37
|
+
standards and versions, and the API reference.
|
|
38
|
+
|
|
39
|
+
.. image:: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml/badge.svg?branch=master
|
|
40
|
+
:target: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml
|
|
41
|
+
:alt: Testing Status
|
|
42
|
+
|
|
43
|
+
.. image:: https://readthedocs.org/projects/python-aqi/badge/?version=latest
|
|
44
|
+
:target: https://readthedocs.org/projects/python-aqi/?badge=latest
|
|
45
|
+
:alt: Documentation Status
|
|
46
|
+
|
|
47
|
+
.. image:: http://img.shields.io/badge/license-BSD%203--Clause-blue.svg
|
|
48
|
+
:target: http://opensource.org/licenses/BSD-3-Clause
|
|
49
|
+
:alt: license BSD 3-Clause
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Install
|
|
53
|
+
=======
|
|
54
|
+
|
|
55
|
+
With pip::
|
|
56
|
+
|
|
57
|
+
$ pip install python-aqi
|
|
58
|
+
|
|
59
|
+
With `uv <https://docs.astral.sh/uv/>`_, to add it to a project::
|
|
60
|
+
|
|
61
|
+
$ uv add python-aqi
|
|
62
|
+
|
|
63
|
+
or to install the ``aqi`` command line tool::
|
|
64
|
+
|
|
65
|
+
$ uv tool install python-aqi
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Usage
|
|
69
|
+
=====
|
|
70
|
+
|
|
71
|
+
Library
|
|
72
|
+
-------
|
|
73
|
+
|
|
74
|
+
Convert a pollutant to its IAQI (Intermediate Air Quality Index)::
|
|
75
|
+
|
|
76
|
+
import aqi
|
|
77
|
+
myaqi = aqi.to_iaqi(aqi.POLLUTANT_PM25, '12', algo=aqi.ALGO_EPA)
|
|
78
|
+
|
|
79
|
+
Get an AQI out of several pollutant concentrations, default algorithm is EPA::
|
|
80
|
+
|
|
81
|
+
import aqi
|
|
82
|
+
myaqi = aqi.to_aqi([
|
|
83
|
+
(aqi.POLLUTANT_PM25, '12'),
|
|
84
|
+
(aqi.POLLUTANT_PM10, '24'),
|
|
85
|
+
(aqi.POLLUTANT_O3_8H, '0.087')
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
The algorithm is a standard and its version: ``'epa:2012'``, or ``'epa'`` for
|
|
89
|
+
the latest version of the family (``aqi.ALGO_EPA``, ``aqi.ALGO_EPA_2012`` and
|
|
90
|
+
the matching ``MEP`` constants are available). Old versions stay selectable, the
|
|
91
|
+
default is always the latest. ``aqi.available_algos()`` lists them all::
|
|
92
|
+
|
|
93
|
+
aqi.available_algos()
|
|
94
|
+
# ['epa:2012', 'epa:2015', 'epa:2024', 'mep:2012', 'mep:2026']
|
|
95
|
+
|
|
96
|
+
The EPA versions are named after the revision that changed the table:
|
|
97
|
+
|
|
98
|
+
* ``epa:2012``: what python-aqi computed until 0.6, PM2.5 Good up to 12.0
|
|
99
|
+
µg/m³ and the 1997 ozone 8 hour breakpoints
|
|
100
|
+
* ``epa:2015``: new ozone 8 hour breakpoints (0.054, 0.070, 0.085, 0.105, 0.200
|
|
101
|
+
ppm), 1 hour ozone only above 0.125 ppm
|
|
102
|
+
* ``epa:2024``: new PM2.5 breakpoints (Good up to 9.0 µg/m³) and a single
|
|
103
|
+
Hazardous category, 301 to 500, for every pollutant. This is the default.
|
|
104
|
+
|
|
105
|
+
The MEP (China) versions follow the standards. The name ``MEP`` stands for the
|
|
106
|
+
Ministry of Environmental Protection, which published HJ 633-2012. The ministry
|
|
107
|
+
was renamed Ministry of Ecology and Environment (MEE) in 2018 and publishes
|
|
108
|
+
HJ 633-2026 under that name, the library keeps ``MEP`` for all versions so that
|
|
109
|
+
the name used by existing code does not change:
|
|
110
|
+
|
|
111
|
+
* ``mep:2012``: HJ 633-2012, IAQI interpolated between the limits of the
|
|
112
|
+
table and IAQI and AQI rounded up, concentrations used as given
|
|
113
|
+
* ``mep:2026``: HJ 633-2026, effective 1 March 2026. The limits of IAQI 100
|
|
114
|
+
change for PM10 (150 to 120 µg/m³) and PM2.5 (75 to 60 µg/m³), and the
|
|
115
|
+
calculation follows the standard: IAQI interpolated between the limits of
|
|
116
|
+
the table, IAQI and AQI rounded up, concentrations rounded to integers (CO to
|
|
117
|
+
one decimal). This is the default for MEP.
|
|
118
|
+
|
|
119
|
+
``mep:2012`` used to be laid out like the EPA tables and now follows
|
|
120
|
+
HJ 633-2012 (``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP_2012)``
|
|
121
|
+
is 82, it was 81). The default changed with the 2024 and 2026 versions: the same PM2.5 concentration can
|
|
122
|
+
now give a different AQI, ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '12.1')`` is 57,
|
|
123
|
+
it was 51, and ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP)`` is
|
|
124
|
+
100, it was 81. Use ``algo=aqi.ALGO_EPA_2012`` to keep the old EPA results.
|
|
125
|
+
|
|
126
|
+
1 hour ozone has no value under 0.125 ppm in the EPA table. ``to_aqi`` ignores
|
|
127
|
+
it when other pollutants are given (the standard says to disregard it), for
|
|
128
|
+
``to_iaqi`` and ``to_cc`` it raises ``aqi.NotApplicableError``, an
|
|
129
|
+
``OutOfRangeError``.
|
|
130
|
+
|
|
131
|
+
Convert an IAQI to its pollutant concentration::
|
|
132
|
+
|
|
133
|
+
import aqi
|
|
134
|
+
mycc = aqi.to_cc(aqi.POLLUTANT_PM25, '22', algo=aqi.ALGO_EPA)
|
|
135
|
+
|
|
136
|
+
Out of range values
|
|
137
|
+
~~~~~~~~~~~~~~~~~~~
|
|
138
|
+
|
|
139
|
+
A concentration (or an AQI) that is not covered by the algorithm raises
|
|
140
|
+
``aqi.OutOfRangeError``, a subclass of ``ValueError`` that carries ``elem``,
|
|
141
|
+
``value``, ``low`` and ``high``. Values are never capped: a capped value would
|
|
142
|
+
hide how far above the scale a measure is, so what to do with it is up to the
|
|
143
|
+
calling code::
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
myaqi = aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')])
|
|
147
|
+
except aqi.OutOfRangeError as err:
|
|
148
|
+
print(err.elem, err.value, err.high)
|
|
149
|
+
|
|
150
|
+
The EPA calls AQI values above 500 "Beyond the AQI" and explains how to
|
|
151
|
+
compute them: use the regular formula with the Hazardous breakpoints. This is
|
|
152
|
+
available on request with ``extrapolate=True``::
|
|
153
|
+
|
|
154
|
+
aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')], extrapolate=True) # 1445
|
|
155
|
+
|
|
156
|
+
This only applies to the EPA algorithm and to pollutants whose table covers
|
|
157
|
+
the whole scale. 8 hour ozone, for example, stops at 300 because the EPA uses
|
|
158
|
+
1 hour ozone above that. The MEP standard has no such method, so ``extrapolate=True``
|
|
159
|
+
still raises with it. HJ 633-2026 does fix the IAQI of two pollutants above
|
|
160
|
+
their table: SO2 1 hour above 800 µg/m³ is 200 and O3 8 hour above 800 µg/m³
|
|
161
|
+
is 300 (HJ 633-2012 said they are not computed: ``mep:2012`` raises
|
|
162
|
+
``aqi.NotApplicableError`` and ``to_aqi`` leaves them out).
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
Command line
|
|
166
|
+
------------
|
|
167
|
+
|
|
168
|
+
List supported algorithms and pollutants::
|
|
169
|
+
|
|
170
|
+
$ aqi -l
|
|
171
|
+
epa:2012: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
|
|
172
|
+
epa:2015: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
|
|
173
|
+
epa:2024 (latest): o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
|
|
174
|
+
mep:2012: so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
|
|
175
|
+
mep:2026 (latest): so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
|
|
176
|
+
|
|
177
|
+
Pick the version of a standard with ``family:version``, ``epa`` alone means the
|
|
178
|
+
latest version, the module names (``aqi.algos.epa``) still work and also mean
|
|
179
|
+
the latest::
|
|
180
|
+
|
|
181
|
+
$ aqi epa:2012 pm25:12
|
|
182
|
+
50
|
|
183
|
+
|
|
184
|
+
Convert PM2.5 to IAQI using EPA algorithm::
|
|
185
|
+
|
|
186
|
+
$ aqi aqi.algos.epa pm25:12
|
|
187
|
+
56
|
|
188
|
+
|
|
189
|
+
Convert PM2.5 to IAQI using EPA algorithm (full length)::
|
|
190
|
+
|
|
191
|
+
$ aqi -c aqi aqi.algos.epa pm25:12
|
|
192
|
+
56
|
|
193
|
+
|
|
194
|
+
Convert pollutants concentrations to AQI using EPA algorithm::
|
|
195
|
+
|
|
196
|
+
$ aqi aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
|
|
197
|
+
122
|
|
198
|
+
|
|
199
|
+
Convert pollutants concentrations to AQI using EPA algorithm, display IAQIs::
|
|
200
|
+
|
|
201
|
+
$ aqi -v aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
|
|
202
|
+
pm25:114 o3_8h:122 co_8h:90
|
|
203
|
+
122
|
|
204
|
+
|
|
205
|
+
Convert PM2.5 IAQI to concentration using EPA algorithm::
|
|
206
|
+
|
|
207
|
+
$ aqi -c cc aqi.algos.epa pm25:39
|
|
208
|
+
7.0
|
|
209
|
+
|
|
210
|
+
Out of range values are an error, the message goes to stderr and the exit
|
|
211
|
+
code is 3::
|
|
212
|
+
|
|
213
|
+
$ aqi aqi.algos.epa pm25:800
|
|
214
|
+
Out of range: pm25: 800.0 is not covered by the algorithm (valid range is 0.0 to 325.4)
|
|
215
|
+
|
|
216
|
+
``-e`` computes EPA values beyond the scale ("Beyond the AQI")::
|
|
217
|
+
|
|
218
|
+
$ aqi -e aqi.algos.epa pm25:800
|
|
219
|
+
1445
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
Development
|
|
223
|
+
===========
|
|
224
|
+
|
|
225
|
+
Set up the development environment (ruff, pre-commit, hatch, coverage)::
|
|
226
|
+
|
|
227
|
+
$ uv sync
|
|
228
|
+
$ uv run pre-commit install
|
|
229
|
+
|
|
230
|
+
Code is linted and formatted with `ruff <https://docs.astral.sh/ruff/>`_
|
|
231
|
+
through `pre-commit <https://pre-commit.com/>`_, the checks run on each
|
|
232
|
+
commit. To run them on all files::
|
|
233
|
+
|
|
234
|
+
$ uv run pre-commit run --all-files
|
|
235
|
+
|
|
236
|
+
Test the package::
|
|
237
|
+
|
|
238
|
+
$ uv run python -m unittest discover
|
|
239
|
+
|
|
240
|
+
The same checks and the tests on Python 3.9 to 3.14 run on GitHub Actions
|
|
241
|
+
(``.github/workflows/ci.yml``) for each push and pull request.
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
Release
|
|
245
|
+
=======
|
|
246
|
+
|
|
247
|
+
A release is made from ``master`` with a clean tree, after adding a
|
|
248
|
+
``X.Y.Z (unreleased)`` section to ``docs/changelog.rst``::
|
|
249
|
+
|
|
250
|
+
$ make release SEGMENT=major # or minor, patch (the default)
|
|
251
|
+
|
|
252
|
+
It runs, in this order:
|
|
253
|
+
|
|
254
|
+
``make bump``
|
|
255
|
+
runs the tests, bumps the version with ``hatch version``, refreshes
|
|
256
|
+
``uv.lock``, dates the changelog section, commits ``release: bump to
|
|
257
|
+
X.Y.Z`` and tags ``X.Y.Z`` (no ``v``).
|
|
258
|
+
``make publish``
|
|
259
|
+
builds the tagged commit and uploads it to PyPI with ``uv publish``. The
|
|
260
|
+
token is a PyPI API token in the ``UV_PUBLISH_TOKEN`` environment
|
|
261
|
+
variable, it is not stored in the repository.
|
|
262
|
+
``make push``
|
|
263
|
+
pushes the commit and the tag to ``origin`` (``REMOTE=...`` to change it)
|
|
264
|
+
once the upload worked.
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
Project
|
|
268
|
+
=======
|
|
269
|
+
|
|
270
|
+
* `Source code on github <https://github.com/hrbonz/python-aqi>`_
|
|
271
|
+
* `Documentation on readthedocs <https://python-aqi.readthedocs.io/>`_
|
|
272
|
+
* `Package on pypi <https://pypi.python.org/pypi/python-aqi>`_
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
Resources
|
|
276
|
+
=========
|
|
277
|
+
|
|
278
|
+
* EPA AQI: Technical Assistance Document for the Reporting of Daily Air
|
|
279
|
+
Quality – the Air Quality Index (AQI) December 2013) found at http://www.epa.gov/airnow/aqi-technical-assistance-document-dec2013.pdf
|
|
280
|
+
* National Ambient Air Quality Standards for Particulate Matter found at http://www.gpo.gov/fdsys/pkg/FR-2013-01-15/pdf/2012-30946.pdf
|
|
281
|
+
* MEP AQI:
|
|
282
|
+
|
|
283
|
+
* GB3095—2012 (2012/02/29) found at http://www.mep.gov.cn/gkml/hbb/bwj/201203/t20120302_224147.htm
|
|
284
|
+
* HJ633-2012 (2012/02/29) found at https://www.mee.gov.cn/ywgz/fgbz/bz/bzwb/jcffbz/201203/W020120410332725219541.pdf
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
License
|
|
288
|
+
=======
|
|
289
|
+
|
|
290
|
+
python-aqi is published under a BSD 3-clause license, see the LICENSE file
|
|
291
|
+
distributed with the project.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
###########################
|
|
2
|
+
python-aqi - AQI conversion
|
|
3
|
+
###########################
|
|
4
|
+
|
|
5
|
+
A library to convert between AQI value and pollutant concentration
|
|
6
|
+
(µg/m³ or ppm) using the following algorithms:
|
|
7
|
+
|
|
8
|
+
* United States Environmental Protection Agency (EPA)
|
|
9
|
+
* China Ministry of Environmental Protection (MEP)
|
|
10
|
+
|
|
11
|
+
The versions of each standard are kept, see `the documentation
|
|
12
|
+
<https://python-aqi.readthedocs.io/>`_ for how an AQI is computed, the
|
|
13
|
+
standards and versions, and the API reference.
|
|
14
|
+
|
|
15
|
+
.. image:: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml/badge.svg?branch=master
|
|
16
|
+
:target: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml
|
|
17
|
+
:alt: Testing Status
|
|
18
|
+
|
|
19
|
+
.. image:: https://readthedocs.org/projects/python-aqi/badge/?version=latest
|
|
20
|
+
:target: https://readthedocs.org/projects/python-aqi/?badge=latest
|
|
21
|
+
:alt: Documentation Status
|
|
22
|
+
|
|
23
|
+
.. image:: http://img.shields.io/badge/license-BSD%203--Clause-blue.svg
|
|
24
|
+
:target: http://opensource.org/licenses/BSD-3-Clause
|
|
25
|
+
:alt: license BSD 3-Clause
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Install
|
|
29
|
+
=======
|
|
30
|
+
|
|
31
|
+
With pip::
|
|
32
|
+
|
|
33
|
+
$ pip install python-aqi
|
|
34
|
+
|
|
35
|
+
With `uv <https://docs.astral.sh/uv/>`_, to add it to a project::
|
|
36
|
+
|
|
37
|
+
$ uv add python-aqi
|
|
38
|
+
|
|
39
|
+
or to install the ``aqi`` command line tool::
|
|
40
|
+
|
|
41
|
+
$ uv tool install python-aqi
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
Usage
|
|
45
|
+
=====
|
|
46
|
+
|
|
47
|
+
Library
|
|
48
|
+
-------
|
|
49
|
+
|
|
50
|
+
Convert a pollutant to its IAQI (Intermediate Air Quality Index)::
|
|
51
|
+
|
|
52
|
+
import aqi
|
|
53
|
+
myaqi = aqi.to_iaqi(aqi.POLLUTANT_PM25, '12', algo=aqi.ALGO_EPA)
|
|
54
|
+
|
|
55
|
+
Get an AQI out of several pollutant concentrations, default algorithm is EPA::
|
|
56
|
+
|
|
57
|
+
import aqi
|
|
58
|
+
myaqi = aqi.to_aqi([
|
|
59
|
+
(aqi.POLLUTANT_PM25, '12'),
|
|
60
|
+
(aqi.POLLUTANT_PM10, '24'),
|
|
61
|
+
(aqi.POLLUTANT_O3_8H, '0.087')
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
The algorithm is a standard and its version: ``'epa:2012'``, or ``'epa'`` for
|
|
65
|
+
the latest version of the family (``aqi.ALGO_EPA``, ``aqi.ALGO_EPA_2012`` and
|
|
66
|
+
the matching ``MEP`` constants are available). Old versions stay selectable, the
|
|
67
|
+
default is always the latest. ``aqi.available_algos()`` lists them all::
|
|
68
|
+
|
|
69
|
+
aqi.available_algos()
|
|
70
|
+
# ['epa:2012', 'epa:2015', 'epa:2024', 'mep:2012', 'mep:2026']
|
|
71
|
+
|
|
72
|
+
The EPA versions are named after the revision that changed the table:
|
|
73
|
+
|
|
74
|
+
* ``epa:2012``: what python-aqi computed until 0.6, PM2.5 Good up to 12.0
|
|
75
|
+
µg/m³ and the 1997 ozone 8 hour breakpoints
|
|
76
|
+
* ``epa:2015``: new ozone 8 hour breakpoints (0.054, 0.070, 0.085, 0.105, 0.200
|
|
77
|
+
ppm), 1 hour ozone only above 0.125 ppm
|
|
78
|
+
* ``epa:2024``: new PM2.5 breakpoints (Good up to 9.0 µg/m³) and a single
|
|
79
|
+
Hazardous category, 301 to 500, for every pollutant. This is the default.
|
|
80
|
+
|
|
81
|
+
The MEP (China) versions follow the standards. The name ``MEP`` stands for the
|
|
82
|
+
Ministry of Environmental Protection, which published HJ 633-2012. The ministry
|
|
83
|
+
was renamed Ministry of Ecology and Environment (MEE) in 2018 and publishes
|
|
84
|
+
HJ 633-2026 under that name, the library keeps ``MEP`` for all versions so that
|
|
85
|
+
the name used by existing code does not change:
|
|
86
|
+
|
|
87
|
+
* ``mep:2012``: HJ 633-2012, IAQI interpolated between the limits of the
|
|
88
|
+
table and IAQI and AQI rounded up, concentrations used as given
|
|
89
|
+
* ``mep:2026``: HJ 633-2026, effective 1 March 2026. The limits of IAQI 100
|
|
90
|
+
change for PM10 (150 to 120 µg/m³) and PM2.5 (75 to 60 µg/m³), and the
|
|
91
|
+
calculation follows the standard: IAQI interpolated between the limits of
|
|
92
|
+
the table, IAQI and AQI rounded up, concentrations rounded to integers (CO to
|
|
93
|
+
one decimal). This is the default for MEP.
|
|
94
|
+
|
|
95
|
+
``mep:2012`` used to be laid out like the EPA tables and now follows
|
|
96
|
+
HJ 633-2012 (``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP_2012)``
|
|
97
|
+
is 82, it was 81). The default changed with the 2024 and 2026 versions: the same PM2.5 concentration can
|
|
98
|
+
now give a different AQI, ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '12.1')`` is 57,
|
|
99
|
+
it was 51, and ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP)`` is
|
|
100
|
+
100, it was 81. Use ``algo=aqi.ALGO_EPA_2012`` to keep the old EPA results.
|
|
101
|
+
|
|
102
|
+
1 hour ozone has no value under 0.125 ppm in the EPA table. ``to_aqi`` ignores
|
|
103
|
+
it when other pollutants are given (the standard says to disregard it), for
|
|
104
|
+
``to_iaqi`` and ``to_cc`` it raises ``aqi.NotApplicableError``, an
|
|
105
|
+
``OutOfRangeError``.
|
|
106
|
+
|
|
107
|
+
Convert an IAQI to its pollutant concentration::
|
|
108
|
+
|
|
109
|
+
import aqi
|
|
110
|
+
mycc = aqi.to_cc(aqi.POLLUTANT_PM25, '22', algo=aqi.ALGO_EPA)
|
|
111
|
+
|
|
112
|
+
Out of range values
|
|
113
|
+
~~~~~~~~~~~~~~~~~~~
|
|
114
|
+
|
|
115
|
+
A concentration (or an AQI) that is not covered by the algorithm raises
|
|
116
|
+
``aqi.OutOfRangeError``, a subclass of ``ValueError`` that carries ``elem``,
|
|
117
|
+
``value``, ``low`` and ``high``. Values are never capped: a capped value would
|
|
118
|
+
hide how far above the scale a measure is, so what to do with it is up to the
|
|
119
|
+
calling code::
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
myaqi = aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')])
|
|
123
|
+
except aqi.OutOfRangeError as err:
|
|
124
|
+
print(err.elem, err.value, err.high)
|
|
125
|
+
|
|
126
|
+
The EPA calls AQI values above 500 "Beyond the AQI" and explains how to
|
|
127
|
+
compute them: use the regular formula with the Hazardous breakpoints. This is
|
|
128
|
+
available on request with ``extrapolate=True``::
|
|
129
|
+
|
|
130
|
+
aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')], extrapolate=True) # 1445
|
|
131
|
+
|
|
132
|
+
This only applies to the EPA algorithm and to pollutants whose table covers
|
|
133
|
+
the whole scale. 8 hour ozone, for example, stops at 300 because the EPA uses
|
|
134
|
+
1 hour ozone above that. The MEP standard has no such method, so ``extrapolate=True``
|
|
135
|
+
still raises with it. HJ 633-2026 does fix the IAQI of two pollutants above
|
|
136
|
+
their table: SO2 1 hour above 800 µg/m³ is 200 and O3 8 hour above 800 µg/m³
|
|
137
|
+
is 300 (HJ 633-2012 said they are not computed: ``mep:2012`` raises
|
|
138
|
+
``aqi.NotApplicableError`` and ``to_aqi`` leaves them out).
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
Command line
|
|
142
|
+
------------
|
|
143
|
+
|
|
144
|
+
List supported algorithms and pollutants::
|
|
145
|
+
|
|
146
|
+
$ aqi -l
|
|
147
|
+
epa:2012: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
|
|
148
|
+
epa:2015: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
|
|
149
|
+
epa:2024 (latest): o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
|
|
150
|
+
mep:2012: so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
|
|
151
|
+
mep:2026 (latest): so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
|
|
152
|
+
|
|
153
|
+
Pick the version of a standard with ``family:version``, ``epa`` alone means the
|
|
154
|
+
latest version, the module names (``aqi.algos.epa``) still work and also mean
|
|
155
|
+
the latest::
|
|
156
|
+
|
|
157
|
+
$ aqi epa:2012 pm25:12
|
|
158
|
+
50
|
|
159
|
+
|
|
160
|
+
Convert PM2.5 to IAQI using EPA algorithm::
|
|
161
|
+
|
|
162
|
+
$ aqi aqi.algos.epa pm25:12
|
|
163
|
+
56
|
|
164
|
+
|
|
165
|
+
Convert PM2.5 to IAQI using EPA algorithm (full length)::
|
|
166
|
+
|
|
167
|
+
$ aqi -c aqi aqi.algos.epa pm25:12
|
|
168
|
+
56
|
|
169
|
+
|
|
170
|
+
Convert pollutants concentrations to AQI using EPA algorithm::
|
|
171
|
+
|
|
172
|
+
$ aqi aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
|
|
173
|
+
122
|
|
174
|
+
|
|
175
|
+
Convert pollutants concentrations to AQI using EPA algorithm, display IAQIs::
|
|
176
|
+
|
|
177
|
+
$ aqi -v aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
|
|
178
|
+
pm25:114 o3_8h:122 co_8h:90
|
|
179
|
+
122
|
|
180
|
+
|
|
181
|
+
Convert PM2.5 IAQI to concentration using EPA algorithm::
|
|
182
|
+
|
|
183
|
+
$ aqi -c cc aqi.algos.epa pm25:39
|
|
184
|
+
7.0
|
|
185
|
+
|
|
186
|
+
Out of range values are an error, the message goes to stderr and the exit
|
|
187
|
+
code is 3::
|
|
188
|
+
|
|
189
|
+
$ aqi aqi.algos.epa pm25:800
|
|
190
|
+
Out of range: pm25: 800.0 is not covered by the algorithm (valid range is 0.0 to 325.4)
|
|
191
|
+
|
|
192
|
+
``-e`` computes EPA values beyond the scale ("Beyond the AQI")::
|
|
193
|
+
|
|
194
|
+
$ aqi -e aqi.algos.epa pm25:800
|
|
195
|
+
1445
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
Development
|
|
199
|
+
===========
|
|
200
|
+
|
|
201
|
+
Set up the development environment (ruff, pre-commit, hatch, coverage)::
|
|
202
|
+
|
|
203
|
+
$ uv sync
|
|
204
|
+
$ uv run pre-commit install
|
|
205
|
+
|
|
206
|
+
Code is linted and formatted with `ruff <https://docs.astral.sh/ruff/>`_
|
|
207
|
+
through `pre-commit <https://pre-commit.com/>`_, the checks run on each
|
|
208
|
+
commit. To run them on all files::
|
|
209
|
+
|
|
210
|
+
$ uv run pre-commit run --all-files
|
|
211
|
+
|
|
212
|
+
Test the package::
|
|
213
|
+
|
|
214
|
+
$ uv run python -m unittest discover
|
|
215
|
+
|
|
216
|
+
The same checks and the tests on Python 3.9 to 3.14 run on GitHub Actions
|
|
217
|
+
(``.github/workflows/ci.yml``) for each push and pull request.
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
Release
|
|
221
|
+
=======
|
|
222
|
+
|
|
223
|
+
A release is made from ``master`` with a clean tree, after adding a
|
|
224
|
+
``X.Y.Z (unreleased)`` section to ``docs/changelog.rst``::
|
|
225
|
+
|
|
226
|
+
$ make release SEGMENT=major # or minor, patch (the default)
|
|
227
|
+
|
|
228
|
+
It runs, in this order:
|
|
229
|
+
|
|
230
|
+
``make bump``
|
|
231
|
+
runs the tests, bumps the version with ``hatch version``, refreshes
|
|
232
|
+
``uv.lock``, dates the changelog section, commits ``release: bump to
|
|
233
|
+
X.Y.Z`` and tags ``X.Y.Z`` (no ``v``).
|
|
234
|
+
``make publish``
|
|
235
|
+
builds the tagged commit and uploads it to PyPI with ``uv publish``. The
|
|
236
|
+
token is a PyPI API token in the ``UV_PUBLISH_TOKEN`` environment
|
|
237
|
+
variable, it is not stored in the repository.
|
|
238
|
+
``make push``
|
|
239
|
+
pushes the commit and the tag to ``origin`` (``REMOTE=...`` to change it)
|
|
240
|
+
once the upload worked.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
Project
|
|
244
|
+
=======
|
|
245
|
+
|
|
246
|
+
* `Source code on github <https://github.com/hrbonz/python-aqi>`_
|
|
247
|
+
* `Documentation on readthedocs <https://python-aqi.readthedocs.io/>`_
|
|
248
|
+
* `Package on pypi <https://pypi.python.org/pypi/python-aqi>`_
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
Resources
|
|
252
|
+
=========
|
|
253
|
+
|
|
254
|
+
* EPA AQI: Technical Assistance Document for the Reporting of Daily Air
|
|
255
|
+
Quality – the Air Quality Index (AQI) December 2013) found at http://www.epa.gov/airnow/aqi-technical-assistance-document-dec2013.pdf
|
|
256
|
+
* National Ambient Air Quality Standards for Particulate Matter found at http://www.gpo.gov/fdsys/pkg/FR-2013-01-15/pdf/2012-30946.pdf
|
|
257
|
+
* MEP AQI:
|
|
258
|
+
|
|
259
|
+
* GB3095—2012 (2012/02/29) found at http://www.mep.gov.cn/gkml/hbb/bwj/201203/t20120302_224147.htm
|
|
260
|
+
* HJ633-2012 (2012/02/29) found at https://www.mee.gov.cn/ywgz/fgbz/bz/bzwb/jcffbz/201203/W020120410332725219541.pdf
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
License
|
|
264
|
+
=======
|
|
265
|
+
|
|
266
|
+
python-aqi is published under a BSD 3-clause license, see the LICENSE file
|
|
267
|
+
distributed with the project.
|