windrex 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.
- windrex-0.1.0/LICENSE +21 -0
- windrex-0.1.0/PKG-INFO +97 -0
- windrex-0.1.0/README.md +50 -0
- windrex-0.1.0/pyproject.toml +100 -0
- windrex-0.1.0/setup.cfg +4 -0
- windrex-0.1.0/src/windrex/__init__.py +57 -0
- windrex-0.1.0/src/windrex/analysis/__init__.py +9 -0
- windrex-0.1.0/src/windrex/analysis/grid.py +117 -0
- windrex-0.1.0/src/windrex/analysis/mcda.py +143 -0
- windrex-0.1.0/src/windrex/config.py +118 -0
- windrex-0.1.0/src/windrex/core/__init__.py +15 -0
- windrex-0.1.0/src/windrex/core/capacity_factor.py +144 -0
- windrex-0.1.0/src/windrex/core/shear.py +80 -0
- windrex-0.1.0/src/windrex/core/temporal.py +83 -0
- windrex-0.1.0/src/windrex/core/wake.py +192 -0
- windrex-0.1.0/src/windrex/core/weibull.py +108 -0
- windrex-0.1.0/src/windrex/core/wind_rose.py +83 -0
- windrex-0.1.0/src/windrex/data/__init__.py +11 -0
- windrex-0.1.0/src/windrex/data/era5.py +146 -0
- windrex-0.1.0/src/windrex/data/lulc.py +164 -0
- windrex-0.1.0/src/windrex/data/nasa_power.py +101 -0
- windrex-0.1.0/src/windrex/data/open_meteo.py +89 -0
- windrex-0.1.0/src/windrex/data/terrain.py +208 -0
- windrex-0.1.0/src/windrex/data/turbine_db.py +184 -0
- windrex-0.1.0/src/windrex/economics/__init__.py +15 -0
- windrex-0.1.0/src/windrex/economics/financial.py +143 -0
- windrex-0.1.0/src/windrex/economics/sensitivity.py +54 -0
- windrex-0.1.0/src/windrex/regional/__init__.py +7 -0
- windrex-0.1.0/src/windrex/regional/analyzer.py +300 -0
- windrex-0.1.0/src/windrex/regional/zones.py +139 -0
- windrex-0.1.0/src/windrex/results.py +95 -0
- windrex-0.1.0/src/windrex.egg-info/PKG-INFO +97 -0
- windrex-0.1.0/src/windrex.egg-info/SOURCES.txt +40 -0
- windrex-0.1.0/src/windrex.egg-info/dependency_links.txt +1 -0
- windrex-0.1.0/src/windrex.egg-info/requires.txt +26 -0
- windrex-0.1.0/src/windrex.egg-info/top_level.txt +1 -0
- windrex-0.1.0/tests/test_capacity_factor.py +49 -0
- windrex-0.1.0/tests/test_financial.py +39 -0
- windrex-0.1.0/tests/test_mcda.py +56 -0
- windrex-0.1.0/tests/test_wake.py +47 -0
- windrex-0.1.0/tests/test_weibull.py +60 -0
- windrex-0.1.0/tests/test_wind_rose.py +39 -0
windrex-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WindreX Development Team
|
|
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.
|
windrex-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: windrex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WindreX - Wind Resource eXchange: wind resource assessment, wake modeling, and site analysis
|
|
5
|
+
Author: WindreX Development Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/windrex-dev/windrex
|
|
8
|
+
Project-URL: Repository, https://github.com/windrex-dev/windrex
|
|
9
|
+
Keywords: wind energy,wind resource,renewable energy,Weibull,wake model,MCDA,capacity factor
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.22
|
|
25
|
+
Requires-Dist: scipy>=1.8
|
|
26
|
+
Requires-Dist: requests>=2.28
|
|
27
|
+
Provides-Extra: era5
|
|
28
|
+
Requires-Dist: atlite>=0.2.10; extra == "era5"
|
|
29
|
+
Requires-Dist: xarray>=0.19; extra == "era5"
|
|
30
|
+
Requires-Dist: dask>=2022.1; extra == "era5"
|
|
31
|
+
Provides-Extra: viz
|
|
32
|
+
Requires-Dist: matplotlib>=3.4; extra == "viz"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
37
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
39
|
+
Provides-Extra: all
|
|
40
|
+
Requires-Dist: atlite>=0.2.10; extra == "all"
|
|
41
|
+
Requires-Dist: xarray>=0.19; extra == "all"
|
|
42
|
+
Requires-Dist: dask>=2022.1; extra == "all"
|
|
43
|
+
Requires-Dist: matplotlib>=3.4; extra == "all"
|
|
44
|
+
Requires-Dist: pytest>=7.0; extra == "all"
|
|
45
|
+
Requires-Dist: pytest-cov>=4.0; extra == "all"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# WindreX — Wind Resource eXchange
|
|
49
|
+
|
|
50
|
+
[](https://github.com/msotocalvo/windrex/actions/workflows/tests.yml)
|
|
51
|
+
[](https://doi.org/10.5281/zenodo.18898424)
|
|
52
|
+
|
|
53
|
+
A Python library for wind resource assessment, wake modeling, turbine database
|
|
54
|
+
management, MCDA-based site suitability analysis, and capacity factor computation.
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **Weibull Analysis**: MLE fitting, PDF, mean power density
|
|
59
|
+
- **Wind Rose**: Directional frequency and speed distributions
|
|
60
|
+
- **Wind Shear**: Power-law extrapolation to hub height
|
|
61
|
+
- **Wake Modeling**: Jensen/Park single-wake deficit, array efficiency with RSS superposition
|
|
62
|
+
- **Capacity Factor**: Hourly CF from Open-Meteo, NASA POWER, or ERA5 data
|
|
63
|
+
- **Turbine Database**: atlite YAML + OEDB REST API
|
|
64
|
+
- **MCDA**: Multi-criteria site suitability (entropy, PCA, manual weights)
|
|
65
|
+
- **Economics**: LCOE, NPV, IRR, sensitivity analysis
|
|
66
|
+
- **Regional Analysis**: Grid-based resource assessment with development zones
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install windrex
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
With ERA5 support:
|
|
75
|
+
```bash
|
|
76
|
+
pip install windrex[era5]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from windrex import fit_weibull, weibull_pdf, compute_wind_rose
|
|
83
|
+
import numpy as np
|
|
84
|
+
|
|
85
|
+
# Fit Weibull to measured speeds
|
|
86
|
+
speeds = np.random.weibull(2.0, 10000) * 7.0
|
|
87
|
+
k, A = fit_weibull(speeds)
|
|
88
|
+
print(f"Weibull k={k:.2f}, A={A:.2f}")
|
|
89
|
+
|
|
90
|
+
# Compute wind rose
|
|
91
|
+
directions = np.random.uniform(0, 360, len(speeds))
|
|
92
|
+
rose = compute_wind_rose(speeds, directions)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
windrex-0.1.0/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# WindreX — Wind Resource eXchange
|
|
2
|
+
|
|
3
|
+
[](https://github.com/msotocalvo/windrex/actions/workflows/tests.yml)
|
|
4
|
+
[](https://doi.org/10.5281/zenodo.18898424)
|
|
5
|
+
|
|
6
|
+
A Python library for wind resource assessment, wake modeling, turbine database
|
|
7
|
+
management, MCDA-based site suitability analysis, and capacity factor computation.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Weibull Analysis**: MLE fitting, PDF, mean power density
|
|
12
|
+
- **Wind Rose**: Directional frequency and speed distributions
|
|
13
|
+
- **Wind Shear**: Power-law extrapolation to hub height
|
|
14
|
+
- **Wake Modeling**: Jensen/Park single-wake deficit, array efficiency with RSS superposition
|
|
15
|
+
- **Capacity Factor**: Hourly CF from Open-Meteo, NASA POWER, or ERA5 data
|
|
16
|
+
- **Turbine Database**: atlite YAML + OEDB REST API
|
|
17
|
+
- **MCDA**: Multi-criteria site suitability (entropy, PCA, manual weights)
|
|
18
|
+
- **Economics**: LCOE, NPV, IRR, sensitivity analysis
|
|
19
|
+
- **Regional Analysis**: Grid-based resource assessment with development zones
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install windrex
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With ERA5 support:
|
|
28
|
+
```bash
|
|
29
|
+
pip install windrex[era5]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from windrex import fit_weibull, weibull_pdf, compute_wind_rose
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
# Fit Weibull to measured speeds
|
|
39
|
+
speeds = np.random.weibull(2.0, 10000) * 7.0
|
|
40
|
+
k, A = fit_weibull(speeds)
|
|
41
|
+
print(f"Weibull k={k:.2f}, A={A:.2f}")
|
|
42
|
+
|
|
43
|
+
# Compute wind rose
|
|
44
|
+
directions = np.random.uniform(0, 360, len(speeds))
|
|
45
|
+
rose = compute_wind_rose(speeds, directions)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "windrex"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "WindreX - Wind Resource eXchange: wind resource assessment, wake modeling, and site analysis"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "WindreX Development Team"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"wind energy",
|
|
17
|
+
"wind resource",
|
|
18
|
+
"renewable energy",
|
|
19
|
+
"Weibull",
|
|
20
|
+
"wake model",
|
|
21
|
+
"MCDA",
|
|
22
|
+
"capacity factor",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Topic :: Scientific/Engineering",
|
|
35
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
dependencies = [
|
|
39
|
+
"numpy>=1.22",
|
|
40
|
+
"scipy>=1.8",
|
|
41
|
+
"requests>=2.28",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
era5 = [
|
|
46
|
+
"atlite>=0.2.10",
|
|
47
|
+
"xarray>=0.19",
|
|
48
|
+
"dask>=2022.1",
|
|
49
|
+
]
|
|
50
|
+
viz = ["matplotlib>=3.4"]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=7.0",
|
|
53
|
+
"pytest-cov>=4.0",
|
|
54
|
+
"ruff>=0.1",
|
|
55
|
+
"build>=1.0",
|
|
56
|
+
"twine>=4.0",
|
|
57
|
+
]
|
|
58
|
+
all = [
|
|
59
|
+
"atlite>=0.2.10",
|
|
60
|
+
"xarray>=0.19",
|
|
61
|
+
"dask>=2022.1",
|
|
62
|
+
"matplotlib>=3.4",
|
|
63
|
+
"pytest>=7.0",
|
|
64
|
+
"pytest-cov>=4.0",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[project.urls]
|
|
68
|
+
Homepage = "https://github.com/windrex-dev/windrex"
|
|
69
|
+
Repository = "https://github.com/windrex-dev/windrex"
|
|
70
|
+
|
|
71
|
+
[tool.setuptools.packages.find]
|
|
72
|
+
where = ["src"]
|
|
73
|
+
include = ["windrex*"]
|
|
74
|
+
exclude = ["tests*"]
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
minversion = "7.0"
|
|
78
|
+
addopts = "-ra -q --strict-markers"
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
filterwarnings = [
|
|
81
|
+
"ignore::DeprecationWarning",
|
|
82
|
+
"ignore::UserWarning",
|
|
83
|
+
]
|
|
84
|
+
markers = [
|
|
85
|
+
"slow: marks tests as slow",
|
|
86
|
+
"integration: marks tests as integration tests",
|
|
87
|
+
"requires_atlite: marks tests that require atlite",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff]
|
|
91
|
+
target-version = "py39"
|
|
92
|
+
line-length = 100
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint]
|
|
95
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP"]
|
|
96
|
+
ignore = ["E501", "B008", "C901"]
|
|
97
|
+
|
|
98
|
+
[tool.ruff.lint.per-file-ignores]
|
|
99
|
+
"__init__.py" = ["F401"]
|
|
100
|
+
"tests/*" = ["B011"]
|
windrex-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WindreX — Wind Resource eXchange
|
|
3
|
+
|
|
4
|
+
A Python library for wind resource assessment, wake modeling, turbine database
|
|
5
|
+
management, and capacity factor computation.
|
|
6
|
+
|
|
7
|
+
Modules:
|
|
8
|
+
- windrex.core: Weibull, wind rose, shear, temporal, wake, capacity factor
|
|
9
|
+
- windrex.data: Open-Meteo, NASA POWER, ERA5, terrain, LULC, turbine DB
|
|
10
|
+
- windrex.economics: Financial analysis and LCOE sensitivity
|
|
11
|
+
- windrex.analysis: MCDA engine and grid evaluation
|
|
12
|
+
- windrex.regional: Regional analyzer and zone generation
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__author__ = "WindreX Development Team"
|
|
17
|
+
|
|
18
|
+
from .core.weibull import fit_weibull, weibull_pdf, weibull_mean_power_density
|
|
19
|
+
from .core.wind_rose import compute_wind_rose, WindRoseData
|
|
20
|
+
from .core.shear import compute_wind_shear, extrapolate_speed
|
|
21
|
+
from .core.temporal import compute_diurnal_pattern, compute_seasonal_pattern
|
|
22
|
+
from .core.wake import jensen_wake_deficit, compute_array_efficiency, compute_spacing_curve
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
# Weibull
|
|
27
|
+
"fit_weibull",
|
|
28
|
+
"weibull_pdf",
|
|
29
|
+
"weibull_mean_power_density",
|
|
30
|
+
# Wind rose
|
|
31
|
+
"compute_wind_rose",
|
|
32
|
+
"WindRoseData",
|
|
33
|
+
# Shear
|
|
34
|
+
"compute_wind_shear",
|
|
35
|
+
"extrapolate_speed",
|
|
36
|
+
# Temporal
|
|
37
|
+
"compute_diurnal_pattern",
|
|
38
|
+
"compute_seasonal_pattern",
|
|
39
|
+
# Wake
|
|
40
|
+
"jensen_wake_deficit",
|
|
41
|
+
"compute_array_efficiency",
|
|
42
|
+
"compute_spacing_curve",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __getattr__(name):
|
|
47
|
+
"""Lazy import for heavy modules."""
|
|
48
|
+
if name == "compute_wind_hourly_cf":
|
|
49
|
+
from .core.capacity_factor import compute_wind_hourly_cf
|
|
50
|
+
return compute_wind_hourly_cf
|
|
51
|
+
if name == "WindAnalyzer":
|
|
52
|
+
from .regional.analyzer import WindAnalyzer
|
|
53
|
+
return WindAnalyzer
|
|
54
|
+
if name == "compute_wind_financials":
|
|
55
|
+
from .economics.financial import compute_wind_financials
|
|
56
|
+
return compute_wind_financials
|
|
57
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Grid evaluation and zone generation for wind assessment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_grid(
|
|
11
|
+
bounds: tuple[float, float, float, float],
|
|
12
|
+
resolution: float = 0.25,
|
|
13
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
14
|
+
"""Build regular lat/lon grid within domain bounds.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
bounds : tuple
|
|
19
|
+
(south, west, north, east) in degrees.
|
|
20
|
+
resolution : float
|
|
21
|
+
Grid spacing in degrees.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
tuple[np.ndarray, np.ndarray]
|
|
26
|
+
(lats, lons) — 1D arrays of grid coordinates.
|
|
27
|
+
"""
|
|
28
|
+
south, west, north, east = bounds
|
|
29
|
+
lats = np.arange(south + resolution / 2, north, resolution)
|
|
30
|
+
lons = np.arange(west + resolution / 2, east, resolution)
|
|
31
|
+
if len(lats) == 0:
|
|
32
|
+
lats = np.array([(south + north) / 2])
|
|
33
|
+
if len(lons) == 0:
|
|
34
|
+
lons = np.array([(west + east) / 2])
|
|
35
|
+
return lats, lons
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def compute_distance_to_grid(
|
|
39
|
+
grid_points: list[dict],
|
|
40
|
+
transmission_lines: list[dict],
|
|
41
|
+
bounds: tuple[float, float, float, float],
|
|
42
|
+
) -> list[float]:
|
|
43
|
+
"""Compute minimum distance from each grid point to nearest transmission line.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
grid_points : list[dict]
|
|
48
|
+
List of dicts with 'lat' and 'lon' keys.
|
|
49
|
+
transmission_lines : list[dict]
|
|
50
|
+
List of line dicts with 'coords' key (list of [lat, lon] pairs).
|
|
51
|
+
bounds : tuple
|
|
52
|
+
(south, west, north, east) for latitude correction.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
list[float]
|
|
57
|
+
Distance in km per grid point.
|
|
58
|
+
"""
|
|
59
|
+
n = len(grid_points)
|
|
60
|
+
if not transmission_lines:
|
|
61
|
+
return [0.0] * n
|
|
62
|
+
|
|
63
|
+
line_coords = []
|
|
64
|
+
for line in transmission_lines:
|
|
65
|
+
coords = line.get("coords", [])
|
|
66
|
+
for coord in coords:
|
|
67
|
+
line_coords.append((coord[0], coord[1]))
|
|
68
|
+
|
|
69
|
+
if not line_coords:
|
|
70
|
+
return [0.0] * n
|
|
71
|
+
|
|
72
|
+
south, west, north, east = bounds
|
|
73
|
+
lc = np.array(line_coords)
|
|
74
|
+
km_per_deg_lat = 111.32
|
|
75
|
+
mid_lat = (south + north) / 2
|
|
76
|
+
km_per_deg_lon = 111.32 * math.cos(math.radians(mid_lat))
|
|
77
|
+
|
|
78
|
+
pt_lats = np.array([pt["lat"] for pt in grid_points])
|
|
79
|
+
pt_lons = np.array([pt["lon"] for pt in grid_points])
|
|
80
|
+
|
|
81
|
+
dy = (pt_lats[:, None] - lc[None, :, 0]) * km_per_deg_lat
|
|
82
|
+
dx = (pt_lons[:, None] - lc[None, :, 1]) * km_per_deg_lon
|
|
83
|
+
dist = np.sqrt(dx ** 2 + dy ** 2)
|
|
84
|
+
|
|
85
|
+
return np.min(dist, axis=1).tolist()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def wind_cf_from_speed(
|
|
89
|
+
ws_hourly: np.ndarray,
|
|
90
|
+
pc_wind_speeds: list[float],
|
|
91
|
+
pc_power_mw: list[float],
|
|
92
|
+
rated_mw: float,
|
|
93
|
+
) -> float:
|
|
94
|
+
"""Compute mean capacity factor from hourly wind speeds and power curve.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
ws_hourly : np.ndarray
|
|
99
|
+
Hourly wind speed at hub height (m/s).
|
|
100
|
+
pc_wind_speeds : list[float]
|
|
101
|
+
Power curve wind speeds (m/s).
|
|
102
|
+
pc_power_mw : list[float]
|
|
103
|
+
Power curve output (MW).
|
|
104
|
+
rated_mw : float
|
|
105
|
+
Turbine rated power (MW).
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
float
|
|
110
|
+
Mean capacity factor.
|
|
111
|
+
"""
|
|
112
|
+
if not pc_wind_speeds or not pc_power_mw or rated_mw <= 0:
|
|
113
|
+
return 0.0
|
|
114
|
+
|
|
115
|
+
ws = np.asarray(ws_hourly, dtype=float)
|
|
116
|
+
power_out = np.interp(ws, pc_wind_speeds, pc_power_mw, left=0.0, right=0.0)
|
|
117
|
+
return float(np.nanmean(power_out) / rated_mw)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Multi-Criteria Decision Analysis (MCDA) engine.
|
|
2
|
+
|
|
3
|
+
Supports three weighting methods:
|
|
4
|
+
- Shannon entropy weights
|
|
5
|
+
- PCA-based weights (first principal component loadings)
|
|
6
|
+
- Manual weights (user-supplied)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_mcda_scores(
|
|
15
|
+
grid_points: list[dict],
|
|
16
|
+
criteria: dict[str, dict],
|
|
17
|
+
method: str = "entropy",
|
|
18
|
+
) -> tuple[np.ndarray, dict[str, float]]:
|
|
19
|
+
"""Compute MCDA composite scores for grid points.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
grid_points : list[dict]
|
|
24
|
+
List of dicts with criterion values as keys.
|
|
25
|
+
criteria : dict[str, dict]
|
|
26
|
+
Criteria configuration. Each key maps to a dict with:
|
|
27
|
+
- 'enabled': bool
|
|
28
|
+
- 'weight': float (for manual method)
|
|
29
|
+
- 'direction': 'maximize' or 'minimize'
|
|
30
|
+
method : str
|
|
31
|
+
Weighting method: 'entropy', 'pca', or 'manual'.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
tuple[np.ndarray, dict[str, float]]
|
|
36
|
+
(scores, computed_weights) — composite scores per grid point
|
|
37
|
+
and the weights used.
|
|
38
|
+
"""
|
|
39
|
+
enabled = {name: c for name, c in criteria.items() if c.get("enabled", True)}
|
|
40
|
+
criteria_names = list(enabled.keys())
|
|
41
|
+
n_cells = len(grid_points)
|
|
42
|
+
n_criteria = len(criteria_names)
|
|
43
|
+
|
|
44
|
+
if n_cells == 0 or n_criteria == 0:
|
|
45
|
+
return np.zeros(n_cells), {}
|
|
46
|
+
|
|
47
|
+
# Build raw matrix
|
|
48
|
+
raw_matrix = np.zeros((n_cells, n_criteria))
|
|
49
|
+
for j, name in enumerate(criteria_names):
|
|
50
|
+
for i, pt in enumerate(grid_points):
|
|
51
|
+
raw_matrix[i, j] = pt.get(name, 0.0)
|
|
52
|
+
|
|
53
|
+
# Min-max normalization
|
|
54
|
+
norm_matrix = np.zeros_like(raw_matrix)
|
|
55
|
+
for j in range(n_criteria):
|
|
56
|
+
col = raw_matrix[:, j]
|
|
57
|
+
col_min, col_max = col.min(), col.max()
|
|
58
|
+
if col_max - col_min > 1e-10:
|
|
59
|
+
norm_matrix[:, j] = (col - col_min) / (col_max - col_min)
|
|
60
|
+
else:
|
|
61
|
+
norm_matrix[:, j] = 0.5
|
|
62
|
+
|
|
63
|
+
# Invert for minimize criteria
|
|
64
|
+
if enabled[criteria_names[j]].get("direction", "maximize") == "minimize":
|
|
65
|
+
norm_matrix[:, j] = 1.0 - norm_matrix[:, j]
|
|
66
|
+
|
|
67
|
+
# Compute weights
|
|
68
|
+
if method == "entropy":
|
|
69
|
+
weights = entropy_weights(norm_matrix)
|
|
70
|
+
elif method == "pca":
|
|
71
|
+
weights = pca_weights(norm_matrix)
|
|
72
|
+
else:
|
|
73
|
+
raw_w = np.array([enabled[name].get("weight", 1.0) for name in criteria_names])
|
|
74
|
+
total_w = raw_w.sum()
|
|
75
|
+
weights = raw_w / total_w if total_w > 0 else np.ones(n_criteria) / n_criteria
|
|
76
|
+
|
|
77
|
+
computed = {name: float(weights[j]) for j, name in enumerate(criteria_names)}
|
|
78
|
+
scores = norm_matrix @ weights
|
|
79
|
+
|
|
80
|
+
return scores, computed
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def entropy_weights(norm_matrix: np.ndarray) -> np.ndarray:
|
|
84
|
+
"""Compute weights using Shannon entropy method.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
norm_matrix : np.ndarray
|
|
89
|
+
Normalized decision matrix, shape (n_alternatives, n_criteria).
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
np.ndarray
|
|
94
|
+
Weights per criterion.
|
|
95
|
+
"""
|
|
96
|
+
n, m = norm_matrix.shape
|
|
97
|
+
if n <= 1:
|
|
98
|
+
return np.ones(m) / m
|
|
99
|
+
|
|
100
|
+
shifted = norm_matrix + 1e-10
|
|
101
|
+
col_sums = shifted.sum(axis=0)
|
|
102
|
+
col_sums[col_sums == 0] = 1
|
|
103
|
+
p = shifted / col_sums
|
|
104
|
+
|
|
105
|
+
k = 1.0 / np.log(n)
|
|
106
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
107
|
+
H = -k * np.nansum(p * np.log(p + 1e-30), axis=0)
|
|
108
|
+
|
|
109
|
+
d = np.maximum(1.0 - H, 0)
|
|
110
|
+
total = d.sum()
|
|
111
|
+
if total > 0:
|
|
112
|
+
return d / total
|
|
113
|
+
return np.ones(m) / m
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pca_weights(norm_matrix: np.ndarray) -> np.ndarray:
|
|
117
|
+
"""Compute weights from first principal component loadings.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
norm_matrix : np.ndarray
|
|
122
|
+
Normalized decision matrix.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
np.ndarray
|
|
127
|
+
Weights per criterion.
|
|
128
|
+
"""
|
|
129
|
+
n, m = norm_matrix.shape
|
|
130
|
+
if n <= m or m <= 1:
|
|
131
|
+
return np.ones(m) / m
|
|
132
|
+
|
|
133
|
+
std = norm_matrix.std(axis=0)
|
|
134
|
+
std[std == 0] = 1
|
|
135
|
+
standardized = (norm_matrix - norm_matrix.mean(axis=0)) / std
|
|
136
|
+
|
|
137
|
+
# SVD-based PCA (no sklearn dependency)
|
|
138
|
+
_, _, Vt = np.linalg.svd(standardized, full_matrices=False)
|
|
139
|
+
loadings = np.abs(Vt[0])
|
|
140
|
+
total = loadings.sum()
|
|
141
|
+
if total > 0:
|
|
142
|
+
return loadings / total
|
|
143
|
+
return np.ones(m) / m
|