suncalc


Namesuncalc JSON
Version 0.1.3 PyPI version JSON
download
home_pagehttps://github.com/kylebarron/suncalc-py
SummaryA fast, vectorized Python port of suncalc.js
upload_time2023-04-19 00:04:17
maintainer
docs_urlNone
authorKyle Barron
requires_python>=3.6
licenseMIT license
keywords suncalc sun
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # suncalc-py

<p>
  <a href="https://github.com/kylebarron/suncalc-py/actions?query=workflow%3ACI" target="_blank">
      <img src="https://github.com/kylebarron/suncalc-py/workflows/test/badge.svg" alt="Test">
  </a>
  <a href="https://pypi.org/project/suncalc" target="_blank">
      <img src="https://img.shields.io/pypi/v/suncalc?color=%2334D058&label=pypi%20package" alt="Package version">
  </a>
  <a href="https://github.com/kylebarron/suncalc-py/blob/master/LICENSE" target="_blank">
      <img src="https://img.shields.io/github/license/kylebarron/suncalc-py.svg" alt="Downloads">
  </a>
</p>


A fast, vectorized Python implementation of [`suncalc.js`][suncalc-js] for
calculating sun position and sunlight phases (times for sunrise, sunset, dusk,
etc.) for the given location and time.

[suncalc-js]: https://github.com/mourner/suncalc

While other similar libraries exist, I didn't originally encounter any that met
my requirements of being openly-licensed, vectorized, and simple to use <sup>1</sup>.

## Install

```
pip install suncalc
```

## Using

### Example

`suncalc` is designed to work both with single values and with arrays of values.

First, import the module:

```py
from suncalc import get_position, get_times
from datetime import datetime
```

There are currently two methods: `get_position`, to get the sun azimuth and
altitude for a given date and position, and `get_times`, to get sunlight phases
for a given date and position.

```py
date = datetime.now()
lon = 20
lat = 45
get_position(date, lon, lat)
# {'azimuth': -0.8619668996997687, 'altitude': 0.5586446727994595}

get_times(date, lon, lat)
# {'solar_noon': Timestamp('2020-11-20 08:47:08.410863770'),
#  'nadir': Timestamp('2020-11-19 20:47:08.410863770'),
#  'sunrise': Timestamp('2020-11-20 03:13:22.645455322'),
#  'sunset': Timestamp('2020-11-20 14:20:54.176272461'),
#  'sunrise_end': Timestamp('2020-11-20 03:15:48.318936035'),
#  'sunset_start': Timestamp('2020-11-20 14:18:28.502791748'),
#  'dawn': Timestamp('2020-11-20 02:50:00.045539551'),
#  'dusk': Timestamp('2020-11-20 14:44:16.776188232'),
#  'nautical_dawn': Timestamp('2020-11-20 02:23:10.019832520'),
#  'nautical_dusk': Timestamp('2020-11-20 15:11:06.801895264'),
#  'night_end': Timestamp('2020-11-20 01:56:36.144269287'),
#  'night': Timestamp('2020-11-20 15:37:40.677458252'),
#  'golden_hour_end': Timestamp('2020-11-20 03:44:46.795967773'),
#  'golden_hour': Timestamp('2020-11-20 13:49:30.025760010')}
```

These methods also work for _arrays_ of data, and since the implementation is
vectorized it's much faster than a for loop in Python.

```py
import pandas as pd

df = pd.DataFrame({
    'date': [date] * 10,
    'lon': [lon] * 10,
    'lat': [lat] * 10
})
pd.DataFrame(get_position(df['date'], df['lon'], df['lat']))
# azimuth	altitude
# 0	-1.485509	-1.048223
# 1	-1.485509	-1.048223
# ...

pd.DataFrame(get_times(df['date'], df['lon'], df['lat']))['solar_noon']
# 0   2020-11-20 08:47:08.410863872+00:00
# 1   2020-11-20 08:47:08.410863872+00:00
# ...
# Name: solar_noon, dtype: datetime64[ns, UTC]
```

If you want to join this data back to your `DataFrame`, you can use `pd.concat`:

```py
times = pd.DataFrame(get_times(df['date'], df['lon'], df['lat']))
pd.concat([df, times], axis=1)
```

### API

#### `get_position`

Calculate sun position (azimuth and altitude) for a given date and
latitude/longitude

- `date` (`datetime` or a pandas series of datetimes): date and time to find sun position of. **Datetime must be in UTC**.
- `lng` (`float` or numpy array of `float`): longitude to find sun position of
- `lat` (`float` or numpy array of `float`): latitude to find sun position of

Returns a `dict` with two keys: `azimuth` and `altitude`. If the input values
were singletons, the `dict`'s values will be floats. Otherwise they'll be numpy
arrays of floats.

#### `get_times`

- `date` (`datetime` or a pandas series of datetimes): date and time to find sunlight phases of. **Datetime must be in UTC**.
- `lng` (`float` or numpy array of `float`): longitude to find sunlight phases of
- `lat` (`float` or numpy array of `float`): latitude to find sunlight phases of
- `height` (`float` or numpy array of `float`, default `0`): observer height in meters
- `times` (`Iterable[Tuple[float, str, str]]`): an iterable defining the angle above the horizon and strings for custom sunlight phases. The default is:

    ```py
    # (angle, morning name, evening name)
    DEFAULT_TIMES = [
        (-0.833, 'sunrise', 'sunset'),
        (-0.3, 'sunrise_end', 'sunset_start'),
        (-6, 'dawn', 'dusk'),
        (-12, 'nautical_dawn', 'nautical_dusk'),
        (-18, 'night_end', 'night'),
        (6, 'golden_hour_end', 'golden_hour')
    ]
    ```

Returns a `dict` where the keys are `solar_noon`, `nadir`, plus any keys passed
in the `times` argument. If the input values were singletons, the `dict`'s
values will be of type `datetime.datetime` (or `pd.Timestamp` if you have pandas
installed, which is a subclass of and therefore compatible with
`datetime.datetime`). Otherwise they'll be pandas `DateTime` series. **The
returned times will be in UTC.**

## Benchmark

This benchmark is to show that the vectorized implementation is nearly 100x
faster than a for loop in Python.

First set up a `DataFrame` with random data. Here I create 100,000 rows.

```py
from suncalc import get_position, get_times
import pandas as pd

def random_dates(start, end, n=10):
    """Create an array of random dates"""
    start_u = start.value//10**9
    end_u = end.value//10**9
    return pd.to_datetime(np.random.randint(start_u, end_u, n), unit='s')

start = pd.to_datetime('2015-01-01')
end = pd.to_datetime('2018-01-01')
dates = random_dates(start, end, n=100_000)

lons = np.random.uniform(low=-179, high=179, size=(100_000,))
lats = np.random.uniform(low=-89, high=89, size=(100_000,))

df = pd.DataFrame({'date': dates, 'lat': lats, 'lon': lons})
```

Then compute `SunCalc.get_position` two ways: the first using the vectorized
implementation and the second using `df.apply`, which is equivalent to a for
loop. The first is more than **100x faster** than the second.

```py
%timeit get_position(df['date'], df['lon'], df['lat'])
# 41.4 ms ± 437 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit df.apply(lambda row: get_position(row['date'], row['lon'], row['lat']), axis=1)
# 4.89 s ± 184 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

Likewise, compute `SunCalc.get_times` the same two ways: first using the
vectorized implementation and the second using `df.apply`. The first is **2800x
faster** than the second! Some of the difference here is that under the hood the
non-vectorized approach uses `pd.to_datetime` while the vectorized
implementation uses `np.astype('datetime64[ns, UTC]')`. `pd.to_datetime` is
really slow!!

```py
%timeit get_times(df['date'], df['lon'], df['lat'])
# 55.3 ms ± 1.91 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%time df.apply(lambda row: get_times(row['date'], row['lon'], row['lat']), axis=1)
# CPU times: user 2min 33s, sys: 288 ms, total: 2min 34s
# Wall time: 2min 34s
```

---

1: [`pyorbital`](https://github.com/pytroll/pyorbital) looks great but is
GPL3-licensed; [`pysolar`](https://github.com/pingswept/pysolar) is also
GPL3-licensed; [`pyEphem`](https://rhodesmill.org/pyephem/) is LGPL3-licensed.
[`suncalcPy`](https://github.com/Broham/suncalcPy) is another port of
`suncalc.js`, and is MIT-licensed, but doesn't use Numpy and thus isn't
vectorized. I recently discovered [`sunpy`](https://github.com/sunpy/sunpy) and
[`astropy`](https://github.com/astropy/astropy), both of which probably would've
worked but I didn't see them at first and they look quite complex for this
simple task...


# Changelog

## [0.1.3] - 2023-04-18

- Ensure pandas 2.0 compatibility (fix integer casting of datetimes)

## [0.1.2] - 2020-12-02

- Try to catch NaN before passing to `datetime.utcfromtimestamp`

## [0.1.1] - 2020-11-20

- Fix PyPI install by adding `MANIFEST.in`
- Update documentation

## [0.1.0] - 2020-11-19

- Initial release on PyPI

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/kylebarron/suncalc-py",
    "name": "suncalc",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": "",
    "keywords": "suncalc,sun",
    "author": "Kyle Barron",
    "author_email": "kylebarron2@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/c2/ff/f66239517089f07ee32aa737069127790c0d7df1f4a12bd0875208dc89d2/suncalc-0.1.3.tar.gz",
    "platform": null,
    "description": "# suncalc-py\n\n<p>\n  <a href=\"https://github.com/kylebarron/suncalc-py/actions?query=workflow%3ACI\" target=\"_blank\">\n      <img src=\"https://github.com/kylebarron/suncalc-py/workflows/test/badge.svg\" alt=\"Test\">\n  </a>\n  <a href=\"https://pypi.org/project/suncalc\" target=\"_blank\">\n      <img src=\"https://img.shields.io/pypi/v/suncalc?color=%2334D058&label=pypi%20package\" alt=\"Package version\">\n  </a>\n  <a href=\"https://github.com/kylebarron/suncalc-py/blob/master/LICENSE\" target=\"_blank\">\n      <img src=\"https://img.shields.io/github/license/kylebarron/suncalc-py.svg\" alt=\"Downloads\">\n  </a>\n</p>\n\n\nA fast, vectorized Python implementation of [`suncalc.js`][suncalc-js] for\ncalculating sun position and sunlight phases (times for sunrise, sunset, dusk,\netc.) for the given location and time.\n\n[suncalc-js]: https://github.com/mourner/suncalc\n\nWhile other similar libraries exist, I didn't originally encounter any that met\nmy requirements of being openly-licensed, vectorized, and simple to use <sup>1</sup>.\n\n## Install\n\n```\npip install suncalc\n```\n\n## Using\n\n### Example\n\n`suncalc` is designed to work both with single values and with arrays of values.\n\nFirst, import the module:\n\n```py\nfrom suncalc import get_position, get_times\nfrom datetime import datetime\n```\n\nThere are currently two methods: `get_position`, to get the sun azimuth and\naltitude for a given date and position, and `get_times`, to get sunlight phases\nfor a given date and position.\n\n```py\ndate = datetime.now()\nlon = 20\nlat = 45\nget_position(date, lon, lat)\n# {'azimuth': -0.8619668996997687, 'altitude': 0.5586446727994595}\n\nget_times(date, lon, lat)\n# {'solar_noon': Timestamp('2020-11-20 08:47:08.410863770'),\n#  'nadir': Timestamp('2020-11-19 20:47:08.410863770'),\n#  'sunrise': Timestamp('2020-11-20 03:13:22.645455322'),\n#  'sunset': Timestamp('2020-11-20 14:20:54.176272461'),\n#  'sunrise_end': Timestamp('2020-11-20 03:15:48.318936035'),\n#  'sunset_start': Timestamp('2020-11-20 14:18:28.502791748'),\n#  'dawn': Timestamp('2020-11-20 02:50:00.045539551'),\n#  'dusk': Timestamp('2020-11-20 14:44:16.776188232'),\n#  'nautical_dawn': Timestamp('2020-11-20 02:23:10.019832520'),\n#  'nautical_dusk': Timestamp('2020-11-20 15:11:06.801895264'),\n#  'night_end': Timestamp('2020-11-20 01:56:36.144269287'),\n#  'night': Timestamp('2020-11-20 15:37:40.677458252'),\n#  'golden_hour_end': Timestamp('2020-11-20 03:44:46.795967773'),\n#  'golden_hour': Timestamp('2020-11-20 13:49:30.025760010')}\n```\n\nThese methods also work for _arrays_ of data, and since the implementation is\nvectorized it's much faster than a for loop in Python.\n\n```py\nimport pandas as pd\n\ndf = pd.DataFrame({\n    'date': [date] * 10,\n    'lon': [lon] * 10,\n    'lat': [lat] * 10\n})\npd.DataFrame(get_position(df['date'], df['lon'], df['lat']))\n# azimuth\taltitude\n# 0\t-1.485509\t-1.048223\n# 1\t-1.485509\t-1.048223\n# ...\n\npd.DataFrame(get_times(df['date'], df['lon'], df['lat']))['solar_noon']\n# 0   2020-11-20 08:47:08.410863872+00:00\n# 1   2020-11-20 08:47:08.410863872+00:00\n# ...\n# Name: solar_noon, dtype: datetime64[ns, UTC]\n```\n\nIf you want to join this data back to your `DataFrame`, you can use `pd.concat`:\n\n```py\ntimes = pd.DataFrame(get_times(df['date'], df['lon'], df['lat']))\npd.concat([df, times], axis=1)\n```\n\n### API\n\n#### `get_position`\n\nCalculate sun position (azimuth and altitude) for a given date and\nlatitude/longitude\n\n- `date` (`datetime` or a pandas series of datetimes): date and time to find sun position of. **Datetime must be in UTC**.\n- `lng` (`float` or numpy array of `float`): longitude to find sun position of\n- `lat` (`float` or numpy array of `float`): latitude to find sun position of\n\nReturns a `dict` with two keys: `azimuth` and `altitude`. If the input values\nwere singletons, the `dict`'s values will be floats. Otherwise they'll be numpy\narrays of floats.\n\n#### `get_times`\n\n- `date` (`datetime` or a pandas series of datetimes): date and time to find sunlight phases of. **Datetime must be in UTC**.\n- `lng` (`float` or numpy array of `float`): longitude to find sunlight phases of\n- `lat` (`float` or numpy array of `float`): latitude to find sunlight phases of\n- `height` (`float` or numpy array of `float`, default `0`): observer height in meters\n- `times` (`Iterable[Tuple[float, str, str]]`): an iterable defining the angle above the horizon and strings for custom sunlight phases. The default is:\n\n    ```py\n    # (angle, morning name, evening name)\n    DEFAULT_TIMES = [\n        (-0.833, 'sunrise', 'sunset'),\n        (-0.3, 'sunrise_end', 'sunset_start'),\n        (-6, 'dawn', 'dusk'),\n        (-12, 'nautical_dawn', 'nautical_dusk'),\n        (-18, 'night_end', 'night'),\n        (6, 'golden_hour_end', 'golden_hour')\n    ]\n    ```\n\nReturns a `dict` where the keys are `solar_noon`, `nadir`, plus any keys passed\nin the `times` argument. If the input values were singletons, the `dict`'s\nvalues will be of type `datetime.datetime` (or `pd.Timestamp` if you have pandas\ninstalled, which is a subclass of and therefore compatible with\n`datetime.datetime`). Otherwise they'll be pandas `DateTime` series. **The\nreturned times will be in UTC.**\n\n## Benchmark\n\nThis benchmark is to show that the vectorized implementation is nearly 100x\nfaster than a for loop in Python.\n\nFirst set up a `DataFrame` with random data. Here I create 100,000 rows.\n\n```py\nfrom suncalc import get_position, get_times\nimport pandas as pd\n\ndef random_dates(start, end, n=10):\n    \"\"\"Create an array of random dates\"\"\"\n    start_u = start.value//10**9\n    end_u = end.value//10**9\n    return pd.to_datetime(np.random.randint(start_u, end_u, n), unit='s')\n\nstart = pd.to_datetime('2015-01-01')\nend = pd.to_datetime('2018-01-01')\ndates = random_dates(start, end, n=100_000)\n\nlons = np.random.uniform(low=-179, high=179, size=(100_000,))\nlats = np.random.uniform(low=-89, high=89, size=(100_000,))\n\ndf = pd.DataFrame({'date': dates, 'lat': lats, 'lon': lons})\n```\n\nThen compute `SunCalc.get_position` two ways: the first using the vectorized\nimplementation and the second using `df.apply`, which is equivalent to a for\nloop. The first is more than **100x faster** than the second.\n\n```py\n%timeit get_position(df['date'], df['lon'], df['lat'])\n# 41.4 ms \u00b1 437 \u00b5s per loop (mean \u00b1 std. dev. of 7 runs, 10 loops each)\n\n%timeit df.apply(lambda row: get_position(row['date'], row['lon'], row['lat']), axis=1)\n# 4.89 s \u00b1 184 ms per loop (mean \u00b1 std. dev. of 7 runs, 1 loop each)\n```\n\nLikewise, compute `SunCalc.get_times` the same two ways: first using the\nvectorized implementation and the second using `df.apply`. The first is **2800x\nfaster** than the second! Some of the difference here is that under the hood the\nnon-vectorized approach uses `pd.to_datetime` while the vectorized\nimplementation uses `np.astype('datetime64[ns, UTC]')`. `pd.to_datetime` is\nreally slow!!\n\n```py\n%timeit get_times(df['date'], df['lon'], df['lat'])\n# 55.3 ms \u00b1 1.91 ms per loop (mean \u00b1 std. dev. of 7 runs, 10 loops each)\n\n%time df.apply(lambda row: get_times(row['date'], row['lon'], row['lat']), axis=1)\n# CPU times: user 2min 33s, sys: 288 ms, total: 2min 34s\n# Wall time: 2min 34s\n```\n\n---\n\n1: [`pyorbital`](https://github.com/pytroll/pyorbital) looks great but is\nGPL3-licensed; [`pysolar`](https://github.com/pingswept/pysolar) is also\nGPL3-licensed; [`pyEphem`](https://rhodesmill.org/pyephem/) is LGPL3-licensed.\n[`suncalcPy`](https://github.com/Broham/suncalcPy) is another port of\n`suncalc.js`, and is MIT-licensed, but doesn't use Numpy and thus isn't\nvectorized. I recently discovered [`sunpy`](https://github.com/sunpy/sunpy) and\n[`astropy`](https://github.com/astropy/astropy), both of which probably would've\nworked but I didn't see them at first and they look quite complex for this\nsimple task...\n\n\n# Changelog\n\n## [0.1.3] - 2023-04-18\n\n- Ensure pandas 2.0 compatibility (fix integer casting of datetimes)\n\n## [0.1.2] - 2020-12-02\n\n- Try to catch NaN before passing to `datetime.utcfromtimestamp`\n\n## [0.1.1] - 2020-11-20\n\n- Fix PyPI install by adding `MANIFEST.in`\n- Update documentation\n\n## [0.1.0] - 2020-11-19\n\n- Initial release on PyPI\n",
    "bugtrack_url": null,
    "license": "MIT license",
    "summary": "A fast, vectorized Python port of suncalc.js",
    "version": "0.1.3",
    "project_urls": {
        "Homepage": "https://github.com/kylebarron/suncalc-py"
    },
    "split_keywords": [
        "suncalc",
        "sun"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c2fff66239517089f07ee32aa737069127790c0d7df1f4a12bd0875208dc89d2",
                "md5": "3898210e0f53be581522e71b41c1b39b",
                "sha256": "9f8834da86870b94ab8c5ab978963d7c44b8f0addce63199e53c5c5c8b0d49f6"
            },
            "downloads": -1,
            "filename": "suncalc-0.1.3.tar.gz",
            "has_sig": false,
            "md5_digest": "3898210e0f53be581522e71b41c1b39b",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 13147,
            "upload_time": "2023-04-19T00:04:17",
            "upload_time_iso_8601": "2023-04-19T00:04:17.665365Z",
            "url": "https://files.pythonhosted.org/packages/c2/ff/f66239517089f07ee32aa737069127790c0d7df1f4a12bd0875208dc89d2/suncalc-0.1.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-04-19 00:04:17",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "kylebarron",
    "github_project": "suncalc-py",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "suncalc"
}
        
Elapsed time: 1.49830s