1.4 Dust Soiling: HSU (Live, Forecast, Historic)
Introduction¶
The following examples shows how to retrieve HSU dust soiling loss using the Solcast Python SDK and visualize the data.
- Live estimated actuals (near real-time and past 7 days)
- Forecast (near real-time and up to 7 days ahead)
- Historic (date-ranged queries; up to 31 days per request)
All requests here use the SDK, returning convenient response objects that can be converted to pandas DataFrames.
Model Background¶
The Humboldt State University (HSU) soiling model reports a soiling ratio (equal to 1 − transmission loss) that evolves at each time step according to local particulate matter (PM) concentrations. Users of the Solcast soiling API can configure rainfall cleaning thresholds, panel tilt, and manual washing schedules. Solcast supplies the precipitation history and PM2.5/PM10 concentrations from our meteorological datasets so the model can be run without sourcing external environmental data. The result is a loss series that tracks changing atmospheric conditions and can feed forecasting or yield assessment workflows. See Coello & Boyle, 2019 (IEEE J. Photovoltaics) for the original formulation.
import os
import pandas as pd
from solcast import live as solcast_live
from solcast import forecast as solcast_forecast
from solcast import historic as solcast_historic
from solcast.unmetered_locations import UNMETERED_LOCATIONS
Configurations¶
API_BASE = os.environ.get("SOLCAST_API_BASE", "https://api.solcast.com.au")
API_KEY = os.environ.get("SOLCAST_API_KEY", "")
# Using unmetered location to avoid API key usage
sydney = UNMETERED_LOCATIONS['Sydney Opera House']
SDK Parameters¶
The following SDK function will be used:
help(solcast_live.soiling_hsu)
Help on function soiling_hsu in module solcast.live:
soiling_hsu(latitude: float, longitude: float, **kwargs) -> solcast.api.PandafiableResponse
Get hourly soiling loss using the HSU model.
Returns a time series of estimated cumulative soiling / cleanliness state for the
requested location based on Solcast's HSU model.
Args:
latitude: Decimal degrees, between -90 and 90 (north positive).
longitude: Decimal degrees, between -180 and 180 (east positive).
**kwargs: Additional query parameters accepted by the endpoint (e.g. depo_veloc_pm10, initial_soiling).
Returns:
PandafiableResponse: Response object; call `.to_pandas()` for a DataFrame.
See https://docs.solcast.com.au/ for full parameter details.
Accessing Additional Parameters¶
For this example, we will provide additional parameters as specified by the Solcast API docs. Following is a brief summary:
- latitude
- longitude
- period: PT5M | PT10M | PT15M | PT20M | PT30M | PT60M (default PT30M)
- tilt: 0 to 90 (optional; tilt in degrees)
- initial_soiling: 0 to 0.3437 (fraction at request start)
- manual_wash_dates: list of ISO 8601 dates when cleaning occurs
- cleaning_threshold: rainfall (mm) in a rolling 24h window to clean (default 1.0)
- hours: for live and forecast, number of hours to retrieve (max 168)
Tip: Use the SDK’s .to_pandas() for quick plotting.
live_params = {
"latitude": sydney.get("latitude"),
"longitude": sydney.get("longitude"),
"manual_wash_dates": "[2022-10-26,2025-11-14,2025-11-26]",
"period": "PT15M",
"initial_soiling": 0.1,
"cleaning_threshold": 1.0,
"hours": 168,
}
print(live_params)
live_resp = solcast_live.soiling_hsu(base_url=API_BASE, api_key=API_KEY, **live_params)
live_resp
{'latitude': -33.856784, 'longitude': 151.215297, 'manual_wash_dates': '[2022-10-26,2025-11-14,2025-11-26]', 'period': 'PT15M', 'initial_soiling': 0.1, 'cleaning_threshold': 1.0, 'hours': 168}
status code=200, url=https://api.solcast.com.au/data/live/soiling/hsu?latitude=-33.856784&longitude=151.215297&format=json&manual_wash_dates=%5B2022-10-26%2C2025-11-14%2C2025-11-26%5D&period=PT15M&initial_soiling=0.1&cleaning_threshold=1.0&hours=168, method=GET
live_df = live_resp.to_pandas()
live_df = live_df.tz_convert('Australia/Sydney')
live_df.head(10)
| hsu_loss_fraction | |
|---|---|
| period_end | |
| 2025-12-26 15:15:00+11:00 | 0.0 |
| 2025-12-26 15:00:00+11:00 | 0.0 |
| 2025-12-26 14:45:00+11:00 | 0.0 |
| 2025-12-26 14:30:00+11:00 | 0.0 |
| 2025-12-26 14:15:00+11:00 | 0.0 |
| 2025-12-26 14:00:00+11:00 | 0.0 |
| 2025-12-26 13:45:00+11:00 | 0.0 |
| 2025-12-26 13:30:00+11:00 | 0.0 |
| 2025-12-26 13:15:00+11:00 | 0.0 |
| 2025-12-26 13:00:00+11:00 | 0.0 |
live_df.plot()
<Axes: xlabel='period_end'>
fc_params = {
"latitude": sydney.get("latitude"),
"longitude": sydney.get("longitude"),
"hours": 72,
"period": "PT15M",
"initial_soiling": 0.0,
"manual_wash_dates": "[2022-10-26,2025-11-14,2025-11-26]",
"cleaning_threshold": 1.0,
}
print(fc_params)
fc_resp = solcast_forecast.soiling_hsu(base_url=API_BASE, api_key=API_KEY, **fc_params)
fc_resp
{'latitude': -33.856784, 'longitude': 151.215297, 'hours': 72, 'period': 'PT15M', 'initial_soiling': 0.0, 'manual_wash_dates': '[2022-10-26,2025-11-14,2025-11-26]', 'cleaning_threshold': 1.0}
status code=200, url=https://api.solcast.com.au/data/forecast/soiling/hsu?latitude=-33.856784&longitude=151.215297&format=json&hours=72&period=PT15M&initial_soiling=0.0&manual_wash_dates=%5B2022-10-26%2C2025-11-14%2C2025-11-26%5D&cleaning_threshold=1.0, method=GET
fc_df = fc_resp.to_pandas()
fc_df = fc_df.tz_convert("Australia/Sydney")
fc_df.head(10)
| hsu_loss_fraction | |
|---|---|
| period_end | |
| 2025-12-26 15:30:00+11:00 | 0.0 |
| 2025-12-26 15:45:00+11:00 | 0.0 |
| 2025-12-26 16:00:00+11:00 | 0.0 |
| 2025-12-26 16:15:00+11:00 | 0.0 |
| 2025-12-26 16:30:00+11:00 | 0.0 |
| 2025-12-26 16:45:00+11:00 | 0.0 |
| 2025-12-26 17:00:00+11:00 | 0.0 |
| 2025-12-26 17:15:00+11:00 | 0.0 |
| 2025-12-26 17:30:00+11:00 | 0.0 |
| 2025-12-26 17:45:00+11:00 | 0.0 |
fc_df.plot()
<Axes: xlabel='period_end'>
hist_base_params = {
"latitude": sydney.get("latitude"),
"longitude": sydney.get("longitude"),
"period": "PT30M",
"start": "2025-10-25T14:45:00Z",
"duration": "P30D",
}
hist_params = hist_base_params | {"output_parameters": "ghi,precipitation_rate,pm2.5,pm10"}
soiling_params = hist_base_params | {
"manual_wash_dates": "[2025-11-03]",
"cleaning_threshold": 2.0,
}
print(hist_params)
hist_resp = solcast_historic.radiation_and_weather(base_url=API_BASE, api_key=API_KEY, **hist_params)
print(soiling_params)
soiling_resp = solcast_historic.soiling_hsu(base_url=API_BASE, api_key=API_KEY, **soiling_params)
soiling_resp
{'latitude': -33.856784, 'longitude': 151.215297, 'period': 'PT30M', 'start': '2025-10-25T14:45:00Z', 'duration': 'P30D', 'output_parameters': 'ghi,precipitation_rate,pm2.5,pm10'}
{'latitude': -33.856784, 'longitude': 151.215297, 'period': 'PT30M', 'start': '2025-10-25T14:45:00Z', 'duration': 'P30D', 'manual_wash_dates': '[2025-11-03]', 'cleaning_threshold': 2.0}
status code=200, url=https://api.solcast.com.au/data/historic/soiling/hsu?latitude=-33.856784&longitude=151.215297&start=2025-10-25T14%3A45%3A00Z&format=json&period=PT30M&manual_wash_dates=%5B2025-11-03%5D&cleaning_threshold=2.0&duration=P30D, method=GET
hist_df = hist_resp.to_pandas()
hist_df = hist_df.tz_convert('Australia/Sydney')
hist_df.head(10)
| ghi | precipitation_rate | pm10 | pm2.5 | |
|---|---|---|---|---|
| period_end | ||||
| 2025-10-26 02:00:00+11:00 | 0 | 0.0 | 26.6 | 10.6 |
| 2025-10-26 02:30:00+11:00 | 0 | 0.0 | 25.2 | 10.3 |
| 2025-10-26 03:00:00+11:00 | 0 | 0.0 | 23.7 | 10.1 |
| 2025-10-26 03:30:00+11:00 | 0 | 0.0 | 22.2 | 9.9 |
| 2025-10-26 04:00:00+11:00 | 0 | 0.0 | 20.8 | 9.7 |
| 2025-10-26 04:30:00+11:00 | 0 | 0.0 | 19.3 | 9.6 |
| 2025-10-26 05:00:00+11:00 | 0 | 0.0 | 18.2 | 9.4 |
| 2025-10-26 05:30:00+11:00 | 0 | 0.0 | 17.4 | 9.3 |
| 2025-10-26 06:00:00+11:00 | 0 | 0.0 | 16.9 | 9.2 |
| 2025-10-26 06:30:00+11:00 | 9 | 0.0 | 16.7 | 9.1 |
soiling_df = soiling_resp.to_pandas()
soiling_df = soiling_df.tz_convert('Australia/Sydney')
soiling_df.head(10)
| hsu_loss_fraction | |
|---|---|
| period_end | |
| 2025-10-26 02:00:00+11:00 | 0.0066 |
| 2025-10-26 02:30:00+11:00 | 0.0066 |
| 2025-10-26 03:00:00+11:00 | 0.0066 |
| 2025-10-26 03:30:00+11:00 | 0.0066 |
| 2025-10-26 04:00:00+11:00 | 0.0066 |
| 2025-10-26 04:30:00+11:00 | 0.0066 |
| 2025-10-26 05:00:00+11:00 | 0.0066 |
| 2025-10-26 05:30:00+11:00 | 0.0066 |
| 2025-10-26 06:00:00+11:00 | 0.0067 |
| 2025-10-26 06:30:00+11:00 | 0.0067 |
precipitation_accum_1D = hist_df['precipitation_rate'].rolling(window="1D", closed="right").sum().div(pd.Timedelta('PT60M') / hist_base_params["period"]).rename("precipitation_accum_1D")
hist_soiling = pd.concat([hist_df, precipitation_accum_1D, soiling_df], axis=1)
ax = hist_soiling.plot(subplots=True, figsize=(10, 12))
ax[4].axhline(soiling_params["cleaning_threshold"], color='red', linestyle='--', label='Cleaning Threshold')
for date in soiling_params["manual_wash_dates"].strip("[]").split(","):
ax[5].axvline(pd.to_datetime(date).tz_localize('UTC').tz_convert('Australia/Sydney'), color='green', linestyle=':', label='Manual Wash Date')
ax[4].legend()
ax[5].legend()
<matplotlib.legend.Legend at 0x73209dd9b950>
Troubleshooting¶
- 401/403: Ensure API key is valid and access includes soiling endpoints.
- Empty payloads: Reduce
hoursor adjuststart/duration. - Parsing mismatch: Inspect
.to_dict()from the SDK response and adjust normalization. - Time zones: Use
.tz_convert()after setting a UTC index.
Tip: For larger periods, paginate historic queries (max 31 days per request) and concatenate the results in pandas.