enerlytics-ai 0.1.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.
- enerlytics_ai-0.1.0/LICENSE +21 -0
- enerlytics_ai-0.1.0/PKG-INFO +142 -0
- enerlytics_ai-0.1.0/README.md +89 -0
- enerlytics_ai-0.1.0/pyproject.toml +42 -0
- enerlytics_ai-0.1.0/setup.cfg +4 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/__init__.py +0 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/api/__init__.py +1 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/api/routes.py +105 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/app/__init__.py +1 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/app/config.py +47 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/app/main.py +11 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/models/__init__.py +1 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/models/energy_model.py +7 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/models/lcoe_model.py +15 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/services/__init__.py +1 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/services/pvgis_data_service.py +148 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/services/solar_data_service.py +188 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/services/solar_model_service.py +35 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/utils/__init__.py +1 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/utils/constants.py +17 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai/utils/helpers.py +11 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai.egg-info/PKG-INFO +142 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai.egg-info/SOURCES.txt +24 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai.egg-info/dependency_links.txt +1 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai.egg-info/requires.txt +11 -0
- enerlytics_ai-0.1.0/src/enerlytics_ai.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Huseyin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: enerlytics-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI platform for predicting renewable energy investment opportunities using meteorological data, machine learning and financial analytics.
|
|
5
|
+
Author-email: Huseyin Kucukogul <huseyinkucukogulcontact@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Huseyin
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/Kucukogul/enerlytics.ai
|
|
29
|
+
Project-URL: Repository, https://github.com/Kucukogul/enerlytics.ai
|
|
30
|
+
Keywords: energy,AI,machine learning,renewable energy,solar,analytics
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Science/Research
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
License-File: LICENSE
|
|
41
|
+
Requires-Dist: fastapi>=0.110.0
|
|
42
|
+
Requires-Dist: uvicorn>=0.27.0
|
|
43
|
+
Requires-Dist: pandas>=2.1.0
|
|
44
|
+
Requires-Dist: numpy>=1.26.0
|
|
45
|
+
Requires-Dist: requests>=2.31.0
|
|
46
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
47
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
48
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
49
|
+
Requires-Dist: shapely>=2.0.0
|
|
50
|
+
Requires-Dist: tqdm>=4.66.0
|
|
51
|
+
Requires-Dist: tenacity>=8.2.0
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# Enerlytics.ai
|
|
55
|
+
|
|
56
|
+
AI platform for predicting renewable energy investment opportunities in Turkiye.
|
|
57
|
+
|
|
58
|
+
Combines meteorological data, machine learning and financial analytics to estimate solar energy potential and evaluate project feasibility at any location in Turkiye.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Setup
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python -m venv .venv && source .venv/bin/activate
|
|
66
|
+
pip install -r requirements.txt
|
|
67
|
+
cp .env.example .env
|
|
68
|
+
uvicorn enerlytics_ai.app.main:app --app-dir src --reload
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
API → `http://127.0.0.1:8000` · Docs → `http://127.0.0.1:8000/docs`
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Site analysis
|
|
79
|
+
curl -X POST "http://127.0.0.1:8000/api/v1/analyze-site" \
|
|
80
|
+
-H "Content-Type: application/json" \
|
|
81
|
+
-d '{"latitude": 39.9208, "longitude": 32.8541}'
|
|
82
|
+
|
|
83
|
+
# Historical solar data
|
|
84
|
+
curl -X POST "http://127.0.0.1:8000/api/v1/historical-solar" \
|
|
85
|
+
-H "Content-Type: application/json" \
|
|
86
|
+
-d '{"latitude": 39.9208, "longitude": 32.8541, "start_year": 2015, "end_year": 2024}'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
enerlytics.ai/
|
|
95
|
+
│
|
|
96
|
+
├── src/enerlytics_ai/
|
|
97
|
+
│ ├── api/
|
|
98
|
+
│ │ └── routes.py # API route definitions
|
|
99
|
+
│ ├── app/
|
|
100
|
+
│ │ ├── main.py # FastAPI app entry point
|
|
101
|
+
│ │ └── config.py # app configuration
|
|
102
|
+
│ ├── models/
|
|
103
|
+
│ │ ├── energy_model.py # solar energy estimation model
|
|
104
|
+
│ │ └── lcoe_model.py # LCOE financial model
|
|
105
|
+
│ ├── services/
|
|
106
|
+
│ │ ├── pvgis_data_service.py # PVGIS API integration
|
|
107
|
+
│ │ ├── solar_data_service.py # solar data fetching & caching
|
|
108
|
+
│ │ └── solar_model_service.py # model inference service
|
|
109
|
+
│ └── utils/
|
|
110
|
+
│ ├── constants.py # shared constants
|
|
111
|
+
│ └── helpers.py # utility functions
|
|
112
|
+
│
|
|
113
|
+
├── pipelines/
|
|
114
|
+
│ ├── province_scan.py # scans all 81 TR provinces
|
|
115
|
+
│ └── scoring.py # investment scoring logic
|
|
116
|
+
│
|
|
117
|
+
├── analysis/
|
|
118
|
+
│ ├── notebooks/
|
|
119
|
+
│ │ ├── 01_eda.ipynb # exploratory data analysis
|
|
120
|
+
│ │ └── 02_turkiye_geneli_eda.ipynb # Turkiye-wide solar EDA
|
|
121
|
+
│ └── outputs/
|
|
122
|
+
│ ├── monthly_stats.csv
|
|
123
|
+
│ ├── tr81_monthly_risk_band.csv
|
|
124
|
+
│ └── tr81_province_ranking.csv # province investment ranking
|
|
125
|
+
│
|
|
126
|
+
├── data/
|
|
127
|
+
│ ├── raw/pvgis/ # raw PVGIS API responses
|
|
128
|
+
│ └── processed/ # cleaned province datasets
|
|
129
|
+
│
|
|
130
|
+
├── scripts/
|
|
131
|
+
│ └── fetch_pvgis_81_seriescalc.py # data collection script
|
|
132
|
+
│
|
|
133
|
+
└── tests/
|
|
134
|
+
├── unit/
|
|
135
|
+
│ └── test_scoring.py
|
|
136
|
+
└── integration/
|
|
137
|
+
└── test_province_pipeline.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
**Author:** Huseyin Kucukogul · MIT License
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Enerlytics.ai
|
|
2
|
+
|
|
3
|
+
AI platform for predicting renewable energy investment opportunities in Turkiye.
|
|
4
|
+
|
|
5
|
+
Combines meteorological data, machine learning and financial analytics to estimate solar energy potential and evaluate project feasibility at any location in Turkiye.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python -m venv .venv && source .venv/bin/activate
|
|
13
|
+
pip install -r requirements.txt
|
|
14
|
+
cp .env.example .env
|
|
15
|
+
uvicorn enerlytics_ai.app.main:app --app-dir src --reload
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
API → `http://127.0.0.1:8000` · Docs → `http://127.0.0.1:8000/docs`
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Site analysis
|
|
26
|
+
curl -X POST "http://127.0.0.1:8000/api/v1/analyze-site" \
|
|
27
|
+
-H "Content-Type: application/json" \
|
|
28
|
+
-d '{"latitude": 39.9208, "longitude": 32.8541}'
|
|
29
|
+
|
|
30
|
+
# Historical solar data
|
|
31
|
+
curl -X POST "http://127.0.0.1:8000/api/v1/historical-solar" \
|
|
32
|
+
-H "Content-Type: application/json" \
|
|
33
|
+
-d '{"latitude": 39.9208, "longitude": 32.8541, "start_year": 2015, "end_year": 2024}'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
enerlytics.ai/
|
|
42
|
+
│
|
|
43
|
+
├── src/enerlytics_ai/
|
|
44
|
+
│ ├── api/
|
|
45
|
+
│ │ └── routes.py # API route definitions
|
|
46
|
+
│ ├── app/
|
|
47
|
+
│ │ ├── main.py # FastAPI app entry point
|
|
48
|
+
│ │ └── config.py # app configuration
|
|
49
|
+
│ ├── models/
|
|
50
|
+
│ │ ├── energy_model.py # solar energy estimation model
|
|
51
|
+
│ │ └── lcoe_model.py # LCOE financial model
|
|
52
|
+
│ ├── services/
|
|
53
|
+
│ │ ├── pvgis_data_service.py # PVGIS API integration
|
|
54
|
+
│ │ ├── solar_data_service.py # solar data fetching & caching
|
|
55
|
+
│ │ └── solar_model_service.py # model inference service
|
|
56
|
+
│ └── utils/
|
|
57
|
+
│ ├── constants.py # shared constants
|
|
58
|
+
│ └── helpers.py # utility functions
|
|
59
|
+
│
|
|
60
|
+
├── pipelines/
|
|
61
|
+
│ ├── province_scan.py # scans all 81 TR provinces
|
|
62
|
+
│ └── scoring.py # investment scoring logic
|
|
63
|
+
│
|
|
64
|
+
├── analysis/
|
|
65
|
+
│ ├── notebooks/
|
|
66
|
+
│ │ ├── 01_eda.ipynb # exploratory data analysis
|
|
67
|
+
│ │ └── 02_turkiye_geneli_eda.ipynb # Turkiye-wide solar EDA
|
|
68
|
+
│ └── outputs/
|
|
69
|
+
│ ├── monthly_stats.csv
|
|
70
|
+
│ ├── tr81_monthly_risk_band.csv
|
|
71
|
+
│ └── tr81_province_ranking.csv # province investment ranking
|
|
72
|
+
│
|
|
73
|
+
├── data/
|
|
74
|
+
│ ├── raw/pvgis/ # raw PVGIS API responses
|
|
75
|
+
│ └── processed/ # cleaned province datasets
|
|
76
|
+
│
|
|
77
|
+
├── scripts/
|
|
78
|
+
│ └── fetch_pvgis_81_seriescalc.py # data collection script
|
|
79
|
+
│
|
|
80
|
+
└── tests/
|
|
81
|
+
├── unit/
|
|
82
|
+
│ └── test_scoring.py
|
|
83
|
+
└── integration/
|
|
84
|
+
└── test_province_pipeline.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
**Author:** Huseyin Kucukogul · MIT License
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "enerlytics-ai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AI platform for predicting renewable energy investment opportunities using meteorological data, machine learning and financial analytics."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Huseyin Kucukogul", email = "huseyinkucukogulcontact@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["energy", "AI", "machine learning", "renewable energy", "solar", "analytics"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"fastapi>=0.110.0",
|
|
25
|
+
"uvicorn>=0.27.0",
|
|
26
|
+
"pandas>=2.1.0",
|
|
27
|
+
"numpy>=1.26.0",
|
|
28
|
+
"requests>=2.31.0",
|
|
29
|
+
"python-dotenv>=1.0.0",
|
|
30
|
+
"pyyaml>=6.0.0",
|
|
31
|
+
"pyarrow>=14.0.0",
|
|
32
|
+
"shapely>=2.0.0",
|
|
33
|
+
"tqdm>=4.66.0",
|
|
34
|
+
"tenacity>=8.2.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/Kucukogul/enerlytics.ai"
|
|
39
|
+
Repository = "https://github.com/Kucukogul/enerlytics.ai"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from requests import exceptions as requests_exceptions
|
|
4
|
+
|
|
5
|
+
from enerlytics_ai.services.solar_data_service import fetch_historical_monthly_ghi_kwh_m2
|
|
6
|
+
from enerlytics_ai.services.solar_model_service import analyze_site
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnalyzeRequest(BaseModel):
|
|
12
|
+
latitude: float = Field(..., ge=-90, le=90)
|
|
13
|
+
longitude: float = Field(..., ge=-180, le=180)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AnalyzeResponse(BaseModel):
|
|
17
|
+
data_source: str
|
|
18
|
+
annual_energy_kwh: float
|
|
19
|
+
estimated_lcoe: float
|
|
20
|
+
lcoe_try_kwh: float
|
|
21
|
+
simple_payback_years: float | None
|
|
22
|
+
summary: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HistoricalSolarRequest(BaseModel):
|
|
26
|
+
latitude: float = Field(..., ge=35.0, le=43.0)
|
|
27
|
+
longitude: float = Field(..., ge=25.0, le=46.0)
|
|
28
|
+
start_year: int = Field(..., ge=1984, le=2100)
|
|
29
|
+
end_year: int = Field(..., ge=1984, le=2100)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HistoricalMonthlyPoint(BaseModel):
|
|
33
|
+
year: int
|
|
34
|
+
month: int
|
|
35
|
+
ghi_kwh_m2_day: float
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HistoricalAnnualSummaryPoint(BaseModel):
|
|
39
|
+
year: int
|
|
40
|
+
total_ghi_kwh_m2_day: float
|
|
41
|
+
average_ghi_kwh_m2_day: float
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class HistoricalSolarResponse(BaseModel):
|
|
45
|
+
data_source: str
|
|
46
|
+
parameter: str
|
|
47
|
+
latitude: float
|
|
48
|
+
longitude: float
|
|
49
|
+
start_year: int
|
|
50
|
+
end_year: int
|
|
51
|
+
monthly_series: list[HistoricalMonthlyPoint]
|
|
52
|
+
annual_summary: list[HistoricalAnnualSummaryPoint]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post("/api/v1/analyze-site", response_model=AnalyzeResponse)
|
|
56
|
+
def analyze(payload: AnalyzeRequest) -> AnalyzeResponse:
|
|
57
|
+
try:
|
|
58
|
+
result = analyze_site(payload.latitude, payload.longitude)
|
|
59
|
+
except requests_exceptions.Timeout as exc:
|
|
60
|
+
raise HTTPException(status_code=504, detail="Upstream solar data request timed out.") from exc
|
|
61
|
+
except requests_exceptions.ConnectionError as exc:
|
|
62
|
+
raise HTTPException(status_code=503, detail="Could not connect to upstream solar data provider.") from exc
|
|
63
|
+
except requests_exceptions.HTTPError as exc:
|
|
64
|
+
raise HTTPException(status_code=502, detail="Upstream solar data provider returned an error.") from exc
|
|
65
|
+
except requests_exceptions.RequestException as exc:
|
|
66
|
+
raise HTTPException(status_code=502, detail="Unexpected upstream request error during analysis.") from exc
|
|
67
|
+
except ValueError as exc:
|
|
68
|
+
raise HTTPException(status_code=422, detail=f"Analysis validation failed: {exc}") from exc
|
|
69
|
+
return AnalyzeResponse(**result)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.post("/api/v1/historical-solar", response_model=HistoricalSolarResponse)
|
|
73
|
+
def historical_solar(payload: HistoricalSolarRequest) -> HistoricalSolarResponse:
|
|
74
|
+
try:
|
|
75
|
+
result = fetch_historical_monthly_ghi_kwh_m2(
|
|
76
|
+
latitude=payload.latitude,
|
|
77
|
+
longitude=payload.longitude,
|
|
78
|
+
start_year=payload.start_year,
|
|
79
|
+
end_year=payload.end_year,
|
|
80
|
+
)
|
|
81
|
+
except requests_exceptions.Timeout as exc:
|
|
82
|
+
raise HTTPException(status_code=504, detail="Upstream solar data request timed out.") from exc
|
|
83
|
+
except requests_exceptions.ConnectionError as exc:
|
|
84
|
+
raise HTTPException(status_code=503, detail="Could not connect to upstream solar data provider.") from exc
|
|
85
|
+
except requests_exceptions.HTTPError as exc:
|
|
86
|
+
status_code = exc.response.status_code if exc.response is not None else "unknown"
|
|
87
|
+
raise HTTPException(
|
|
88
|
+
status_code=502,
|
|
89
|
+
detail=f"Upstream solar data provider returned an error (status: {status_code}).",
|
|
90
|
+
) from exc
|
|
91
|
+
except requests_exceptions.RequestException as exc:
|
|
92
|
+
raise HTTPException(status_code=502, detail="Unexpected upstream request error during historical fetch.") from exc
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
raise HTTPException(status_code=422, detail=f"Historical solar validation failed: {exc}") from exc
|
|
95
|
+
|
|
96
|
+
return HistoricalSolarResponse(
|
|
97
|
+
data_source=result["source"],
|
|
98
|
+
parameter=result["parameter"],
|
|
99
|
+
latitude=result["latitude"],
|
|
100
|
+
longitude=result["longitude"],
|
|
101
|
+
start_year=result["start_year"],
|
|
102
|
+
end_year=result["end_year"],
|
|
103
|
+
monthly_series=result["monthly_series"],
|
|
104
|
+
annual_summary=result["annual_summary"],
|
|
105
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Settings:
|
|
11
|
+
solar_data_provider: str = os.getenv("SOLAR_DATA_PROVIDER", "nasa").lower()
|
|
12
|
+
nasa_power_base_url: str = os.getenv(
|
|
13
|
+
"NASA_POWER_BASE_URL",
|
|
14
|
+
"https://power.larc.nasa.gov/api/temporal/climatology/point",
|
|
15
|
+
)
|
|
16
|
+
nasa_power_monthly_base_url: str = os.getenv(
|
|
17
|
+
"NASA_POWER_MONTHLY_BASE_URL",
|
|
18
|
+
"https://power.larc.nasa.gov/api/temporal/monthly/point",
|
|
19
|
+
)
|
|
20
|
+
nasa_power_parameter: str = os.getenv("NASA_POWER_PARAMETER", "ALLSKY_SFC_SW_DWN")
|
|
21
|
+
nasa_power_community: str = os.getenv("NASA_POWER_COMMUNITY", "RE")
|
|
22
|
+
nasa_power_format: str = os.getenv("NASA_POWER_FORMAT", "JSON")
|
|
23
|
+
request_timeout_seconds: int = int(os.getenv("REQUEST_TIMEOUT_SECONDS", "15"))
|
|
24
|
+
enable_nasa_cache: bool = os.getenv("ENABLE_NASA_CACHE", "true").lower() == "true"
|
|
25
|
+
nasa_cache_dir: str = os.getenv("NASA_CACHE_DIR", "data/raw/nasa_power")
|
|
26
|
+
nasa_cache_ttl_hours: int = int(os.getenv("NASA_CACHE_TTL_HOURS", "720"))
|
|
27
|
+
pvgis_base_url: str = os.getenv("PVGIS_BASE_URL", "https://re.jrc.ec.europa.eu/api/v5_2/MRcalc")
|
|
28
|
+
pvgis_start_year: int = int(os.getenv("PVGIS_START_YEAR", "2015"))
|
|
29
|
+
pvgis_end_year: int = int(os.getenv("PVGIS_END_YEAR", "2020"))
|
|
30
|
+
enable_pvgis_cache: bool = os.getenv("ENABLE_PVGIS_CACHE", "true").lower() == "true"
|
|
31
|
+
pvgis_cache_dir: str = os.getenv("PVGIS_CACHE_DIR", "data/raw/pvgis")
|
|
32
|
+
pvgis_cache_ttl_hours: int = int(os.getenv("PVGIS_CACHE_TTL_HOURS", "720"))
|
|
33
|
+
|
|
34
|
+
panel_efficiency: float = float(os.getenv("PANEL_EFFICIENCY", "0.20"))
|
|
35
|
+
system_losses: float = float(os.getenv("SYSTEM_LOSSES", "0.14"))
|
|
36
|
+
default_system_area_m2: float = float(os.getenv("DEFAULT_SYSTEM_AREA_M2", "25"))
|
|
37
|
+
project_lifetime_years: int = int(os.getenv("PROJECT_LIFETIME_YEARS", "25"))
|
|
38
|
+
|
|
39
|
+
capex_usd: float = float(os.getenv("CAPEX_USD", "12000"))
|
|
40
|
+
opex_annual_usd: float = float(os.getenv("OPEX_ANNUAL_USD", "180"))
|
|
41
|
+
usd_try: float = float(os.getenv("USD_TRY", "32.0"))
|
|
42
|
+
discount_rate_tr: float = float(os.getenv("DISCOUNT_RATE_TR", "0.30"))
|
|
43
|
+
land_cost_try: float = float(os.getenv("LAND_COST_TRY", "0"))
|
|
44
|
+
electricity_sale_price_try_kwh: float = float(os.getenv("ELECTRICITY_SALE_PRICE_TRY_KWH", "2.5"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
settings = Settings()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from enerlytics_ai.app.config import settings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def estimate_annual_energy_kwh(annual_irradiance_kwh_m2: float) -> float:
|
|
5
|
+
raw_output = annual_irradiance_kwh_m2 * settings.default_system_area_m2 * settings.panel_efficiency
|
|
6
|
+
net_output = raw_output * (1 - settings.system_losses)
|
|
7
|
+
return round(net_output, 2)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from enerlytics_ai.app.config import settings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def estimate_lcoe_usd_per_kwh(annual_energy_kwh: float) -> float:
|
|
5
|
+
if annual_energy_kwh <= 0:
|
|
6
|
+
raise ValueError("Annual energy must be greater than zero.")
|
|
7
|
+
if settings.project_lifetime_years <= 0:
|
|
8
|
+
raise ValueError("Project lifetime must be greater than zero years.")
|
|
9
|
+
if settings.capex_usd < 0 or settings.opex_annual_usd < 0:
|
|
10
|
+
raise ValueError("CAPEX and annual OPEX cannot be negative.")
|
|
11
|
+
|
|
12
|
+
total_cost = settings.capex_usd + (settings.opex_annual_usd * settings.project_lifetime_years)
|
|
13
|
+
total_energy_output = annual_energy_kwh * settings.project_lifetime_years
|
|
14
|
+
lcoe = total_cost / total_energy_output
|
|
15
|
+
return round(lcoe, 4)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from enerlytics_ai.app.config import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cache_file_path(latitude: float, longitude: float, start_year: int, end_year: int) -> Path:
|
|
14
|
+
cache_key = (
|
|
15
|
+
f"{latitude:.4f}_{longitude:.4f}_{start_year}_{end_year}".replace("-", "m")
|
|
16
|
+
)
|
|
17
|
+
return Path(settings.pvgis_cache_dir) / f"{cache_key}.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _read_cached_payload(
|
|
21
|
+
latitude: float, longitude: float, start_year: int, end_year: int
|
|
22
|
+
) -> Optional[Dict[str, Any]]:
|
|
23
|
+
if not settings.enable_pvgis_cache:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
cache_path = _cache_file_path(latitude, longitude, start_year, end_year)
|
|
27
|
+
if not cache_path.exists():
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
with cache_path.open("r", encoding="utf-8") as cache_file:
|
|
32
|
+
payload = json.load(cache_file)
|
|
33
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
34
|
+
try:
|
|
35
|
+
cache_path.unlink(missing_ok=True)
|
|
36
|
+
except OSError:
|
|
37
|
+
pass
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
fetched_at = float(payload.get("fetched_at_unix", 0))
|
|
41
|
+
ttl_seconds = settings.pvgis_cache_ttl_hours * 3600
|
|
42
|
+
if ttl_seconds <= 0 or (time.time() - fetched_at) > ttl_seconds:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
monthly_entries = payload.get("monthly_entries")
|
|
46
|
+
if not isinstance(monthly_entries, list) or not monthly_entries:
|
|
47
|
+
return None
|
|
48
|
+
return monthly_entries
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _write_cache_payload(
|
|
52
|
+
latitude: float,
|
|
53
|
+
longitude: float,
|
|
54
|
+
start_year: int,
|
|
55
|
+
end_year: int,
|
|
56
|
+
monthly_entries: List[Dict[str, Any]],
|
|
57
|
+
) -> None:
|
|
58
|
+
if not settings.enable_pvgis_cache:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
cache_path = _cache_file_path(latitude, longitude, start_year, end_year)
|
|
62
|
+
os.makedirs(cache_path.parent, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
with cache_path.open("w", encoding="utf-8") as cache_file:
|
|
65
|
+
json.dump(
|
|
66
|
+
{
|
|
67
|
+
"fetched_at_unix": int(time.time()),
|
|
68
|
+
"monthly_entries": monthly_entries,
|
|
69
|
+
},
|
|
70
|
+
cache_file,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _request_monthly_entries(
|
|
75
|
+
latitude: float, longitude: float, start_year: int, end_year: int
|
|
76
|
+
) -> List[Dict[str, Any]]:
|
|
77
|
+
params = {
|
|
78
|
+
"lat": latitude,
|
|
79
|
+
"lon": longitude,
|
|
80
|
+
"horirrad": 1,
|
|
81
|
+
"outputformat": "json",
|
|
82
|
+
"startyear": start_year,
|
|
83
|
+
"endyear": end_year,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
response = requests.get(
|
|
87
|
+
settings.pvgis_base_url,
|
|
88
|
+
params=params,
|
|
89
|
+
timeout=settings.request_timeout_seconds,
|
|
90
|
+
)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
payload = response.json()
|
|
93
|
+
|
|
94
|
+
monthly_entries = payload.get("outputs", {}).get("monthly", [])
|
|
95
|
+
if not isinstance(monthly_entries, list) or not monthly_entries:
|
|
96
|
+
raise ValueError("PVGIS MRcalc response missing monthly entries.")
|
|
97
|
+
return monthly_entries
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _annual_irradiance_from_monthly_entries(monthly_entries: List[Dict[str, Any]]) -> float:
|
|
101
|
+
yearly_totals: Dict[int, float] = defaultdict(float)
|
|
102
|
+
yearly_month_counts: Dict[int, int] = defaultdict(int)
|
|
103
|
+
|
|
104
|
+
for entry in monthly_entries:
|
|
105
|
+
try:
|
|
106
|
+
year = int(entry["year"])
|
|
107
|
+
value = float(entry["H(h)_m"])
|
|
108
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
109
|
+
raise ValueError("PVGIS monthly entry has invalid format.") from exc
|
|
110
|
+
yearly_totals[year] += value
|
|
111
|
+
yearly_month_counts[year] += 1
|
|
112
|
+
|
|
113
|
+
complete_years = [y for y, count in yearly_month_counts.items() if count == 12]
|
|
114
|
+
if not complete_years:
|
|
115
|
+
raise ValueError("PVGIS response has no complete-year monthly coverage.")
|
|
116
|
+
|
|
117
|
+
yearly_sums = [yearly_totals[year] for year in complete_years]
|
|
118
|
+
return sum(yearly_sums) / len(yearly_sums)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def fetch_annual_ghi_kwh_m2(latitude: float, longitude: float) -> Dict[str, Any]:
|
|
122
|
+
start_year = settings.pvgis_start_year
|
|
123
|
+
end_year = settings.pvgis_end_year
|
|
124
|
+
if start_year > end_year:
|
|
125
|
+
raise ValueError("PVGIS_START_YEAR must be less than or equal to PVGIS_END_YEAR.")
|
|
126
|
+
|
|
127
|
+
cached_entries = _read_cached_payload(latitude, longitude, start_year, end_year)
|
|
128
|
+
if cached_entries is not None:
|
|
129
|
+
annual_ghi = _annual_irradiance_from_monthly_entries(cached_entries)
|
|
130
|
+
return {
|
|
131
|
+
"source": "PVGIS (cache)",
|
|
132
|
+
"annual_irradiance_kwh_m2": round(annual_ghi, 2),
|
|
133
|
+
"monthly_entries": cached_entries,
|
|
134
|
+
"start_year": start_year,
|
|
135
|
+
"end_year": end_year,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
monthly_entries = _request_monthly_entries(latitude, longitude, start_year, end_year)
|
|
139
|
+
_write_cache_payload(latitude, longitude, start_year, end_year, monthly_entries)
|
|
140
|
+
annual_ghi = _annual_irradiance_from_monthly_entries(monthly_entries)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"source": "PVGIS",
|
|
144
|
+
"annual_irradiance_kwh_m2": round(annual_ghi, 2),
|
|
145
|
+
"monthly_entries": monthly_entries,
|
|
146
|
+
"start_year": start_year,
|
|
147
|
+
"end_year": end_year,
|
|
148
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from enerlytics_ai.app.config import settings
|
|
10
|
+
from enerlytics_ai.utils.helpers import monthly_to_annual_irradiance
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cache_file_path(latitude: float, longitude: float) -> Path:
|
|
14
|
+
cache_key = f"{latitude:.4f}_{longitude:.4f}_{settings.nasa_power_parameter}".replace("-", "m")
|
|
15
|
+
return Path(settings.nasa_cache_dir) / f"{cache_key}.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _read_cached_monthly_data(latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
|
|
19
|
+
if not settings.enable_nasa_cache:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
cache_path = _cache_file_path(latitude, longitude)
|
|
23
|
+
if not cache_path.exists():
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with cache_path.open("r", encoding="utf-8") as cache_file:
|
|
28
|
+
payload = json.load(cache_file)
|
|
29
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
30
|
+
try:
|
|
31
|
+
cache_path.unlink(missing_ok=True)
|
|
32
|
+
except OSError:
|
|
33
|
+
pass
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
fetched_at = float(payload.get("fetched_at_unix", 0))
|
|
37
|
+
ttl_seconds = settings.nasa_cache_ttl_hours * 3600
|
|
38
|
+
if ttl_seconds <= 0 or (time.time() - fetched_at) > ttl_seconds:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
monthly_daily_ghi = payload.get("monthly_daily_ghi")
|
|
42
|
+
if not isinstance(monthly_daily_ghi, dict):
|
|
43
|
+
return None
|
|
44
|
+
return monthly_daily_ghi
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _write_cache_payload(latitude: float, longitude: float, monthly_daily_ghi: Dict[str, Any]) -> None:
|
|
48
|
+
if not settings.enable_nasa_cache:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
cache_path = _cache_file_path(latitude, longitude)
|
|
52
|
+
os.makedirs(cache_path.parent, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
with cache_path.open("w", encoding="utf-8") as cache_file:
|
|
55
|
+
json.dump(
|
|
56
|
+
{
|
|
57
|
+
"fetched_at_unix": int(time.time()),
|
|
58
|
+
"monthly_daily_ghi": monthly_daily_ghi,
|
|
59
|
+
},
|
|
60
|
+
cache_file,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fetch_annual_ghi_kwh_m2(latitude: float, longitude: float) -> Dict[str, Any]:
|
|
65
|
+
cached_monthly_data = _read_cached_monthly_data(latitude=latitude, longitude=longitude)
|
|
66
|
+
if cached_monthly_data is not None:
|
|
67
|
+
annual_ghi = monthly_to_annual_irradiance(cached_monthly_data)
|
|
68
|
+
return {
|
|
69
|
+
"source": "NASA POWER (cache)",
|
|
70
|
+
"annual_irradiance_kwh_m2": round(annual_ghi, 2),
|
|
71
|
+
"monthly_daily_ghi": cached_monthly_data,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
params = {
|
|
75
|
+
"parameters": settings.nasa_power_parameter,
|
|
76
|
+
"community": settings.nasa_power_community,
|
|
77
|
+
"longitude": longitude,
|
|
78
|
+
"latitude": latitude,
|
|
79
|
+
"format": settings.nasa_power_format,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
response = requests.get(
|
|
83
|
+
settings.nasa_power_base_url,
|
|
84
|
+
params=params,
|
|
85
|
+
timeout=settings.request_timeout_seconds,
|
|
86
|
+
)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
payload = response.json()
|
|
89
|
+
|
|
90
|
+
monthly_data = (
|
|
91
|
+
payload.get("properties", {})
|
|
92
|
+
.get("parameter", {})
|
|
93
|
+
.get(settings.nasa_power_parameter, {})
|
|
94
|
+
)
|
|
95
|
+
_write_cache_payload(latitude=latitude, longitude=longitude, monthly_daily_ghi=monthly_data)
|
|
96
|
+
annual_ghi = monthly_to_annual_irradiance(monthly_data)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"source": "NASA POWER",
|
|
100
|
+
"annual_irradiance_kwh_m2": round(annual_ghi, 2),
|
|
101
|
+
"monthly_daily_ghi": monthly_data,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fetch_historical_monthly_ghi_kwh_m2(
|
|
106
|
+
latitude: float,
|
|
107
|
+
longitude: float,
|
|
108
|
+
start_year: int,
|
|
109
|
+
end_year: int,
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
if start_year > end_year:
|
|
112
|
+
raise ValueError("start_year must be less than or equal to end_year.")
|
|
113
|
+
|
|
114
|
+
params = {
|
|
115
|
+
"parameters": settings.nasa_power_parameter,
|
|
116
|
+
"community": settings.nasa_power_community,
|
|
117
|
+
"longitude": longitude,
|
|
118
|
+
"latitude": latitude,
|
|
119
|
+
"format": settings.nasa_power_format,
|
|
120
|
+
"start": str(start_year),
|
|
121
|
+
"end": str(end_year),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
response = requests.get(
|
|
125
|
+
settings.nasa_power_monthly_base_url,
|
|
126
|
+
params=params,
|
|
127
|
+
timeout=settings.request_timeout_seconds,
|
|
128
|
+
)
|
|
129
|
+
response.raise_for_status()
|
|
130
|
+
payload = response.json()
|
|
131
|
+
|
|
132
|
+
monthly_data = (
|
|
133
|
+
payload.get("properties", {})
|
|
134
|
+
.get("parameter", {})
|
|
135
|
+
.get(settings.nasa_power_parameter, {})
|
|
136
|
+
)
|
|
137
|
+
if not isinstance(monthly_data, dict):
|
|
138
|
+
raise ValueError("Historical solar data format is invalid.")
|
|
139
|
+
|
|
140
|
+
monthly_series = []
|
|
141
|
+
for key, value in monthly_data.items():
|
|
142
|
+
if not (isinstance(key, str) and len(key) == 6 and key.isdigit()):
|
|
143
|
+
continue
|
|
144
|
+
year = int(key[:4])
|
|
145
|
+
month = int(key[4:])
|
|
146
|
+
if year < start_year or year > end_year or month < 1 or month > 12:
|
|
147
|
+
continue
|
|
148
|
+
monthly_series.append(
|
|
149
|
+
{
|
|
150
|
+
"year": year,
|
|
151
|
+
"month": month,
|
|
152
|
+
"ghi_kwh_m2_day": float(value),
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
monthly_series.sort(key=lambda item: (item["year"], item["month"]))
|
|
157
|
+
if not monthly_series:
|
|
158
|
+
raise ValueError("No historical monthly solar data returned for requested range.")
|
|
159
|
+
|
|
160
|
+
annual_summary_map: Dict[int, Dict[str, float]] = {}
|
|
161
|
+
for point in monthly_series:
|
|
162
|
+
year = point["year"]
|
|
163
|
+
annual_summary_map.setdefault(year, {"sum_ghi_kwh_m2_day": 0.0, "month_count": 0})
|
|
164
|
+
annual_summary_map[year]["sum_ghi_kwh_m2_day"] += point["ghi_kwh_m2_day"]
|
|
165
|
+
annual_summary_map[year]["month_count"] += 1
|
|
166
|
+
|
|
167
|
+
annual_summary = []
|
|
168
|
+
for year in sorted(annual_summary_map):
|
|
169
|
+
year_total = annual_summary_map[year]["sum_ghi_kwh_m2_day"]
|
|
170
|
+
month_count = annual_summary_map[year]["month_count"]
|
|
171
|
+
annual_summary.append(
|
|
172
|
+
{
|
|
173
|
+
"year": year,
|
|
174
|
+
"total_ghi_kwh_m2_day": round(year_total, 4),
|
|
175
|
+
"average_ghi_kwh_m2_day": round(year_total / month_count, 4),
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"source": "NASA POWER",
|
|
181
|
+
"parameter": settings.nasa_power_parameter,
|
|
182
|
+
"latitude": latitude,
|
|
183
|
+
"longitude": longitude,
|
|
184
|
+
"start_year": start_year,
|
|
185
|
+
"end_year": end_year,
|
|
186
|
+
"monthly_series": monthly_series,
|
|
187
|
+
"annual_summary": annual_summary,
|
|
188
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from enerlytics_ai.models.energy_model import estimate_annual_energy_kwh
|
|
2
|
+
from enerlytics_ai.models.lcoe_model import estimate_lcoe_usd_per_kwh
|
|
3
|
+
from enerlytics_ai.app.config import settings
|
|
4
|
+
from enerlytics_ai.services import pvgis_data_service, solar_data_service
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _fetch_solar_data(latitude: float, longitude: float, provider: str | None = None) -> dict:
|
|
8
|
+
selected = (provider or settings.solar_data_provider).lower()
|
|
9
|
+
if selected == "nasa":
|
|
10
|
+
return solar_data_service.fetch_annual_ghi_kwh_m2(latitude=latitude, longitude=longitude)
|
|
11
|
+
if selected == "pvgis":
|
|
12
|
+
return pvgis_data_service.fetch_annual_ghi_kwh_m2(latitude=latitude, longitude=longitude)
|
|
13
|
+
raise ValueError("Invalid solar data provider. Supported values: nasa, pvgis.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def analyze_site(latitude: float, longitude: float, provider: str | None = None) -> dict:
|
|
17
|
+
solar_data = _fetch_solar_data(latitude=latitude, longitude=longitude, provider=provider)
|
|
18
|
+
annual_energy_kwh = estimate_annual_energy_kwh(solar_data["annual_irradiance_kwh_m2"])
|
|
19
|
+
estimated_lcoe_usd = estimate_lcoe_usd_per_kwh(annual_energy_kwh)
|
|
20
|
+
lcoe_try_kwh = round(estimated_lcoe_usd * settings.usd_try, 4)
|
|
21
|
+
annual_revenue_try = annual_energy_kwh * settings.electricity_sale_price_try_kwh
|
|
22
|
+
total_investment_try = (settings.capex_usd * settings.usd_try) + settings.land_cost_try
|
|
23
|
+
simple_payback_years = round(total_investment_try / annual_revenue_try, 2) if annual_revenue_try > 0 else None
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"data_source": solar_data["source"],
|
|
27
|
+
"annual_energy_kwh": annual_energy_kwh,
|
|
28
|
+
"estimated_lcoe": estimated_lcoe_usd,
|
|
29
|
+
"lcoe_try_kwh": lcoe_try_kwh,
|
|
30
|
+
"simple_payback_years": simple_payback_years,
|
|
31
|
+
"summary": (
|
|
32
|
+
f"Estimated annual production is {annual_energy_kwh} kWh with "
|
|
33
|
+
f"an LCOE of {estimated_lcoe_usd} USD/kWh ({lcoe_try_kwh} TRY/kWh)."
|
|
34
|
+
),
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from enerlytics_ai.utils.constants import DAYS_IN_YEAR, MONTHS_IN_YEAR, NASA_MONTH_KEYS
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def monthly_to_annual_irradiance(monthly_daily_ghi: Dict[str, float]) -> float:
|
|
7
|
+
values = [float(monthly_daily_ghi[m]) for m in NASA_MONTH_KEYS if m in monthly_daily_ghi]
|
|
8
|
+
if len(values) != MONTHS_IN_YEAR:
|
|
9
|
+
raise ValueError("Incomplete monthly GHI data from NASA POWER.")
|
|
10
|
+
average_daily_ghi = sum(values) / MONTHS_IN_YEAR
|
|
11
|
+
return average_daily_ghi * DAYS_IN_YEAR
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: enerlytics-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI platform for predicting renewable energy investment opportunities using meteorological data, machine learning and financial analytics.
|
|
5
|
+
Author-email: Huseyin Kucukogul <huseyinkucukogulcontact@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Huseyin
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/Kucukogul/enerlytics.ai
|
|
29
|
+
Project-URL: Repository, https://github.com/Kucukogul/enerlytics.ai
|
|
30
|
+
Keywords: energy,AI,machine learning,renewable energy,solar,analytics
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Science/Research
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
License-File: LICENSE
|
|
41
|
+
Requires-Dist: fastapi>=0.110.0
|
|
42
|
+
Requires-Dist: uvicorn>=0.27.0
|
|
43
|
+
Requires-Dist: pandas>=2.1.0
|
|
44
|
+
Requires-Dist: numpy>=1.26.0
|
|
45
|
+
Requires-Dist: requests>=2.31.0
|
|
46
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
47
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
48
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
49
|
+
Requires-Dist: shapely>=2.0.0
|
|
50
|
+
Requires-Dist: tqdm>=4.66.0
|
|
51
|
+
Requires-Dist: tenacity>=8.2.0
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# Enerlytics.ai
|
|
55
|
+
|
|
56
|
+
AI platform for predicting renewable energy investment opportunities in Turkiye.
|
|
57
|
+
|
|
58
|
+
Combines meteorological data, machine learning and financial analytics to estimate solar energy potential and evaluate project feasibility at any location in Turkiye.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Setup
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python -m venv .venv && source .venv/bin/activate
|
|
66
|
+
pip install -r requirements.txt
|
|
67
|
+
cp .env.example .env
|
|
68
|
+
uvicorn enerlytics_ai.app.main:app --app-dir src --reload
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
API → `http://127.0.0.1:8000` · Docs → `http://127.0.0.1:8000/docs`
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Site analysis
|
|
79
|
+
curl -X POST "http://127.0.0.1:8000/api/v1/analyze-site" \
|
|
80
|
+
-H "Content-Type: application/json" \
|
|
81
|
+
-d '{"latitude": 39.9208, "longitude": 32.8541}'
|
|
82
|
+
|
|
83
|
+
# Historical solar data
|
|
84
|
+
curl -X POST "http://127.0.0.1:8000/api/v1/historical-solar" \
|
|
85
|
+
-H "Content-Type: application/json" \
|
|
86
|
+
-d '{"latitude": 39.9208, "longitude": 32.8541, "start_year": 2015, "end_year": 2024}'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
enerlytics.ai/
|
|
95
|
+
│
|
|
96
|
+
├── src/enerlytics_ai/
|
|
97
|
+
│ ├── api/
|
|
98
|
+
│ │ └── routes.py # API route definitions
|
|
99
|
+
│ ├── app/
|
|
100
|
+
│ │ ├── main.py # FastAPI app entry point
|
|
101
|
+
│ │ └── config.py # app configuration
|
|
102
|
+
│ ├── models/
|
|
103
|
+
│ │ ├── energy_model.py # solar energy estimation model
|
|
104
|
+
│ │ └── lcoe_model.py # LCOE financial model
|
|
105
|
+
│ ├── services/
|
|
106
|
+
│ │ ├── pvgis_data_service.py # PVGIS API integration
|
|
107
|
+
│ │ ├── solar_data_service.py # solar data fetching & caching
|
|
108
|
+
│ │ └── solar_model_service.py # model inference service
|
|
109
|
+
│ └── utils/
|
|
110
|
+
│ ├── constants.py # shared constants
|
|
111
|
+
│ └── helpers.py # utility functions
|
|
112
|
+
│
|
|
113
|
+
├── pipelines/
|
|
114
|
+
│ ├── province_scan.py # scans all 81 TR provinces
|
|
115
|
+
│ └── scoring.py # investment scoring logic
|
|
116
|
+
│
|
|
117
|
+
├── analysis/
|
|
118
|
+
│ ├── notebooks/
|
|
119
|
+
│ │ ├── 01_eda.ipynb # exploratory data analysis
|
|
120
|
+
│ │ └── 02_turkiye_geneli_eda.ipynb # Turkiye-wide solar EDA
|
|
121
|
+
│ └── outputs/
|
|
122
|
+
│ ├── monthly_stats.csv
|
|
123
|
+
│ ├── tr81_monthly_risk_band.csv
|
|
124
|
+
│ └── tr81_province_ranking.csv # province investment ranking
|
|
125
|
+
│
|
|
126
|
+
├── data/
|
|
127
|
+
│ ├── raw/pvgis/ # raw PVGIS API responses
|
|
128
|
+
│ └── processed/ # cleaned province datasets
|
|
129
|
+
│
|
|
130
|
+
├── scripts/
|
|
131
|
+
│ └── fetch_pvgis_81_seriescalc.py # data collection script
|
|
132
|
+
│
|
|
133
|
+
└── tests/
|
|
134
|
+
├── unit/
|
|
135
|
+
│ └── test_scoring.py
|
|
136
|
+
└── integration/
|
|
137
|
+
└── test_province_pipeline.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
**Author:** Huseyin Kucukogul · MIT License
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/enerlytics_ai/__init__.py
|
|
5
|
+
src/enerlytics_ai.egg-info/PKG-INFO
|
|
6
|
+
src/enerlytics_ai.egg-info/SOURCES.txt
|
|
7
|
+
src/enerlytics_ai.egg-info/dependency_links.txt
|
|
8
|
+
src/enerlytics_ai.egg-info/requires.txt
|
|
9
|
+
src/enerlytics_ai.egg-info/top_level.txt
|
|
10
|
+
src/enerlytics_ai/api/__init__.py
|
|
11
|
+
src/enerlytics_ai/api/routes.py
|
|
12
|
+
src/enerlytics_ai/app/__init__.py
|
|
13
|
+
src/enerlytics_ai/app/config.py
|
|
14
|
+
src/enerlytics_ai/app/main.py
|
|
15
|
+
src/enerlytics_ai/models/__init__.py
|
|
16
|
+
src/enerlytics_ai/models/energy_model.py
|
|
17
|
+
src/enerlytics_ai/models/lcoe_model.py
|
|
18
|
+
src/enerlytics_ai/services/__init__.py
|
|
19
|
+
src/enerlytics_ai/services/pvgis_data_service.py
|
|
20
|
+
src/enerlytics_ai/services/solar_data_service.py
|
|
21
|
+
src/enerlytics_ai/services/solar_model_service.py
|
|
22
|
+
src/enerlytics_ai/utils/__init__.py
|
|
23
|
+
src/enerlytics_ai/utils/constants.py
|
|
24
|
+
src/enerlytics_ai/utils/helpers.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
enerlytics_ai
|