rooftex 0.1.0__py3-none-any.whl
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.
- rooftex/__init__.py +38 -0
- rooftex/config.py +77 -0
- rooftex/core/__init__.py +11 -0
- rooftex/core/adoption.py +102 -0
- rooftex/core/potential.py +99 -0
- rooftex/core/profiles.py +166 -0
- rooftex/economics/__init__.py +8 -0
- rooftex/economics/learning_curve.py +114 -0
- rooftex/results.py +77 -0
- rooftex-0.1.0.dist-info/METADATA +82 -0
- rooftex-0.1.0.dist-info/RECORD +14 -0
- rooftex-0.1.0.dist-info/WHEEL +5 -0
- rooftex-0.1.0.dist-info/licenses/LICENSE +21 -0
- rooftex-0.1.0.dist-info/top_level.txt +1 -0
rooftex/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RoofteX — Rooftop Solar eXchange
|
|
3
|
+
|
|
4
|
+
A Python library for rooftop solar potential assessment, adoption dynamics,
|
|
5
|
+
and stochastic availability profile generation.
|
|
6
|
+
|
|
7
|
+
Modules:
|
|
8
|
+
- rooftex.core: Adoption curves, solar profiles, rooftop potential
|
|
9
|
+
- rooftex.economics: Cost learning curves and integration
|
|
10
|
+
- rooftex.config: Configuration dataclasses
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
__author__ = "RoofteX Development Team"
|
|
15
|
+
|
|
16
|
+
from .config import RooftopConfig
|
|
17
|
+
from .results import RooftopResult
|
|
18
|
+
|
|
19
|
+
from .core.adoption import compute_adoption_curve
|
|
20
|
+
from .core.profiles import generate_profiles
|
|
21
|
+
from .core.potential import calculate_potential
|
|
22
|
+
|
|
23
|
+
from .economics.learning_curve import compute_learning_curve, compute_installed_capacity
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"__version__",
|
|
27
|
+
# Config
|
|
28
|
+
"RooftopConfig",
|
|
29
|
+
# Results
|
|
30
|
+
"RooftopResult",
|
|
31
|
+
# Core
|
|
32
|
+
"compute_adoption_curve",
|
|
33
|
+
"generate_profiles",
|
|
34
|
+
"calculate_potential",
|
|
35
|
+
# Economics
|
|
36
|
+
"compute_learning_curve",
|
|
37
|
+
"compute_installed_capacity",
|
|
38
|
+
]
|
rooftex/config.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Configuration dataclasses for RoofteX."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class RooftopConfig:
|
|
11
|
+
"""Configuration for rooftop solar profile generation.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
num_nodes : int
|
|
16
|
+
Number of nodes in the system.
|
|
17
|
+
hours : int
|
|
18
|
+
Number of hours to simulate (default 8760 = 1 year).
|
|
19
|
+
base_year : int
|
|
20
|
+
Base year for adoption calculation.
|
|
21
|
+
target_year : int
|
|
22
|
+
Target year for projections.
|
|
23
|
+
adoption_scenario : str
|
|
24
|
+
Adoption scenario: 'low', 'medium', or 'high'.
|
|
25
|
+
weather_variability : str
|
|
26
|
+
Weather variability level: 'low', 'normal', or 'high'.
|
|
27
|
+
seed : int or None
|
|
28
|
+
Random seed for reproducibility.
|
|
29
|
+
performance_ratio : float
|
|
30
|
+
Overall PV system performance ratio (0-1).
|
|
31
|
+
initial_adoption : list[float] or None
|
|
32
|
+
Initial adoption fraction per node. If None, defaults to 0.05.
|
|
33
|
+
systems_per_node : list[int] or None
|
|
34
|
+
Number of potential rooftop systems per node.
|
|
35
|
+
avg_system_size_kw : list[float] or None
|
|
36
|
+
Average system size in kW per node.
|
|
37
|
+
adoption_rates : dict[str, float] or None
|
|
38
|
+
Custom adoption rate per scenario. Defaults to low=0.05, medium=0.08, high=0.12.
|
|
39
|
+
max_adoption : dict[str, float] or None
|
|
40
|
+
Maximum adoption fraction per scenario. Defaults to low=0.30, medium=0.50, high=0.70.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
num_nodes: int = 1
|
|
44
|
+
hours: int = 8760
|
|
45
|
+
base_year: int = 2024
|
|
46
|
+
target_year: int = 2050
|
|
47
|
+
adoption_scenario: str = "medium"
|
|
48
|
+
weather_variability: str = "normal"
|
|
49
|
+
seed: Optional[int] = None
|
|
50
|
+
performance_ratio: float = 0.75
|
|
51
|
+
initial_adoption: Optional[list[float]] = None
|
|
52
|
+
systems_per_node: Optional[list[int]] = None
|
|
53
|
+
avg_system_size_kw: Optional[list[float]] = None
|
|
54
|
+
adoption_rates: Optional[dict[str, float]] = None
|
|
55
|
+
max_adoption: Optional[dict[str, float]] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class LearningCurveConfig:
|
|
60
|
+
"""Configuration for cost learning curve projections.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
base_cost_per_kw : float
|
|
65
|
+
Base installation cost in $/kW.
|
|
66
|
+
cost_reduction_rate : float
|
|
67
|
+
Annual cost reduction rate (learning rate).
|
|
68
|
+
degradation_rate : float
|
|
69
|
+
Annual panel degradation rate.
|
|
70
|
+
o_and_m_cost : float
|
|
71
|
+
Annual O&M cost in $/kW.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
base_cost_per_kw: float = 1200.0
|
|
75
|
+
cost_reduction_rate: float = 0.08
|
|
76
|
+
degradation_rate: float = 0.005
|
|
77
|
+
o_and_m_cost: float = 20.0
|
rooftex/core/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Core rooftop solar computation modules."""
|
|
2
|
+
|
|
3
|
+
from .adoption import compute_adoption_curve
|
|
4
|
+
from .profiles import generate_profiles
|
|
5
|
+
from .potential import calculate_potential
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"compute_adoption_curve",
|
|
9
|
+
"generate_profiles",
|
|
10
|
+
"calculate_potential",
|
|
11
|
+
]
|
rooftex/core/adoption.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""S-curve adoption dynamics for rooftop solar.
|
|
2
|
+
|
|
3
|
+
Models technology adoption using logistic (S-curve) functions with
|
|
4
|
+
urbanization factors and scenario-dependent parameters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Default adoption rates per scenario
|
|
13
|
+
_DEFAULT_ADOPTION_RATES = {
|
|
14
|
+
"low": 0.05,
|
|
15
|
+
"medium": 0.08,
|
|
16
|
+
"high": 0.12,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Default maximum adoption per scenario
|
|
20
|
+
_DEFAULT_MAX_ADOPTION = {
|
|
21
|
+
"low": 0.30,
|
|
22
|
+
"medium": 0.50,
|
|
23
|
+
"high": 0.70,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compute_adoption_curve(
|
|
28
|
+
num_nodes: int,
|
|
29
|
+
base_year: int,
|
|
30
|
+
target_year: int,
|
|
31
|
+
adoption_scenario: str = "medium",
|
|
32
|
+
initial_adoption: list[float] | None = None,
|
|
33
|
+
adoption_rates: dict[str, float] | None = None,
|
|
34
|
+
max_adoption: dict[str, float] | None = None,
|
|
35
|
+
urbanization_factors: np.ndarray | None = None,
|
|
36
|
+
rng: np.random.Generator | None = None,
|
|
37
|
+
) -> np.ndarray:
|
|
38
|
+
"""Compute adoption factors at the target year using S-curve dynamics.
|
|
39
|
+
|
|
40
|
+
Each node gets a logistic adoption curve influenced by its urbanization
|
|
41
|
+
factor and the chosen scenario.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
num_nodes : int
|
|
46
|
+
Number of nodes.
|
|
47
|
+
base_year : int
|
|
48
|
+
Base year for adoption calculation.
|
|
49
|
+
target_year : int
|
|
50
|
+
Year at which to evaluate adoption.
|
|
51
|
+
adoption_scenario : str
|
|
52
|
+
Scenario: 'low', 'medium', or 'high'.
|
|
53
|
+
initial_adoption : list[float] or None
|
|
54
|
+
Initial adoption fraction per node. Defaults to 0.05.
|
|
55
|
+
adoption_rates : dict[str, float] or None
|
|
56
|
+
Custom adoption rates per scenario.
|
|
57
|
+
max_adoption : dict[str, float] or None
|
|
58
|
+
Maximum adoption fraction per scenario.
|
|
59
|
+
urbanization_factors : np.ndarray or None
|
|
60
|
+
Urbanization factor per node in [0, 1]. If None, drawn from Beta(2,2).
|
|
61
|
+
rng : np.random.Generator or None
|
|
62
|
+
Random number generator.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
np.ndarray
|
|
67
|
+
Adoption factors per node at target_year, shape (num_nodes,).
|
|
68
|
+
"""
|
|
69
|
+
if rng is None:
|
|
70
|
+
rng = np.random.default_rng()
|
|
71
|
+
|
|
72
|
+
rates = adoption_rates or _DEFAULT_ADOPTION_RATES
|
|
73
|
+
max_adopt = max_adoption or _DEFAULT_MAX_ADOPTION
|
|
74
|
+
|
|
75
|
+
adoption_rate = rates.get(adoption_scenario, 0.08)
|
|
76
|
+
max_adoption_val = max_adopt.get(adoption_scenario, 0.50)
|
|
77
|
+
|
|
78
|
+
years_diff = target_year - base_year
|
|
79
|
+
|
|
80
|
+
# Initial adoption per node
|
|
81
|
+
if initial_adoption is not None:
|
|
82
|
+
init = list(initial_adoption)
|
|
83
|
+
while len(init) < num_nodes:
|
|
84
|
+
init.append(init[-1] if init else 0.05)
|
|
85
|
+
else:
|
|
86
|
+
init = [0.05] * num_nodes
|
|
87
|
+
|
|
88
|
+
# Urbanization
|
|
89
|
+
if urbanization_factors is None:
|
|
90
|
+
urbanization_factors = rng.beta(2, 2, num_nodes)
|
|
91
|
+
else:
|
|
92
|
+
urbanization_factors = np.asarray(urbanization_factors)
|
|
93
|
+
|
|
94
|
+
adoption = np.zeros(num_nodes)
|
|
95
|
+
for node in range(num_nodes):
|
|
96
|
+
node_max = min(0.9, max_adoption_val * (0.8 + 0.4 * urbanization_factors[node]))
|
|
97
|
+
mid_point = base_year + years_diff * (0.4 + 0.2 * rng.random())
|
|
98
|
+
growth_rate = adoption_rate * (0.8 + 0.4 * urbanization_factors[node])
|
|
99
|
+
|
|
100
|
+
adoption[node] = node_max / (1 + np.exp(-growth_rate * (target_year - mid_point)))
|
|
101
|
+
|
|
102
|
+
return adoption
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Rooftop solar potential estimation.
|
|
2
|
+
|
|
3
|
+
Estimates maximum rooftop PV capacity from population, dwelling density,
|
|
4
|
+
and roof characteristics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from rooftex.config import RooftopConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calculate_potential(
|
|
18
|
+
population: list[float],
|
|
19
|
+
dwelling_density: float = 0.35,
|
|
20
|
+
avg_roof_area: float = 50.0,
|
|
21
|
+
suitable_fraction: float = 0.3,
|
|
22
|
+
panel_efficiency: float = 0.20,
|
|
23
|
+
solar_irradiance: float = 1000.0,
|
|
24
|
+
) -> list[float]:
|
|
25
|
+
"""Calculate rooftop solar potential based on population.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
population : list[float]
|
|
30
|
+
Population per node.
|
|
31
|
+
dwelling_density : float
|
|
32
|
+
Dwellings per capita.
|
|
33
|
+
avg_roof_area : float
|
|
34
|
+
Average roof area in m².
|
|
35
|
+
suitable_fraction : float
|
|
36
|
+
Fraction of roof suitable for solar.
|
|
37
|
+
panel_efficiency : float
|
|
38
|
+
Solar panel efficiency (0-1).
|
|
39
|
+
solar_irradiance : float
|
|
40
|
+
Peak solar irradiance in W/m².
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
list[float]
|
|
45
|
+
Maximum potential in MW per node.
|
|
46
|
+
"""
|
|
47
|
+
max_potential = []
|
|
48
|
+
for pop in population:
|
|
49
|
+
num_dwellings = pop * dwelling_density
|
|
50
|
+
total_roof_area = num_dwellings * avg_roof_area * suitable_fraction
|
|
51
|
+
peak_power_kw = total_roof_area * panel_efficiency * solar_irradiance / 1000
|
|
52
|
+
max_potential.append(peak_power_kw / 1000) # Convert to MW
|
|
53
|
+
|
|
54
|
+
return max_potential
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def calculate_potential_from_config(
|
|
58
|
+
config: "RooftopConfig",
|
|
59
|
+
urbanization_factors: np.ndarray | None = None,
|
|
60
|
+
rng: np.random.Generator | None = None,
|
|
61
|
+
) -> list[float]:
|
|
62
|
+
"""Calculate potential from config, using system specs or randomized base values.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
config : RooftopConfig
|
|
67
|
+
Configuration with optional systems_per_node and avg_system_size_kw.
|
|
68
|
+
urbanization_factors : np.ndarray or None
|
|
69
|
+
Urbanization factor per node. Used for random potential generation.
|
|
70
|
+
rng : np.random.Generator or None
|
|
71
|
+
Random number generator.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
list[float]
|
|
76
|
+
Maximum potential per node in MW.
|
|
77
|
+
"""
|
|
78
|
+
if rng is None:
|
|
79
|
+
rng = np.random.default_rng()
|
|
80
|
+
|
|
81
|
+
num_nodes = config.num_nodes
|
|
82
|
+
|
|
83
|
+
if config.systems_per_node is not None and config.avg_system_size_kw is not None:
|
|
84
|
+
systems = list(config.systems_per_node)
|
|
85
|
+
sizes = list(config.avg_system_size_kw)
|
|
86
|
+
|
|
87
|
+
while len(systems) < num_nodes:
|
|
88
|
+
systems.append(systems[-1] if systems else 5000)
|
|
89
|
+
while len(sizes) < num_nodes:
|
|
90
|
+
sizes.append(sizes[-1] if sizes else 5.0)
|
|
91
|
+
|
|
92
|
+
return [systems[i] * sizes[i] / 1000 for i in range(num_nodes)]
|
|
93
|
+
|
|
94
|
+
# Random potential based on urbanization
|
|
95
|
+
if urbanization_factors is None:
|
|
96
|
+
urbanization_factors = rng.beta(2, 2, num_nodes)
|
|
97
|
+
|
|
98
|
+
base_potential = 50 + rng.gamma(shape=2.0, scale=30.0, size=num_nodes)
|
|
99
|
+
return list(base_potential * (0.5 + urbanization_factors))
|
rooftex/core/profiles.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Stochastic rooftop solar profile generation.
|
|
2
|
+
|
|
3
|
+
Generates hourly availability profiles with bell-curve solar patterns,
|
|
4
|
+
weather variability, and cloud events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from rooftex.config import RooftopConfig
|
|
14
|
+
from rooftex.core.adoption import compute_adoption_curve
|
|
15
|
+
from rooftex.core.potential import calculate_potential_from_config
|
|
16
|
+
from rooftex.results import RooftopResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Weather variance maps
|
|
20
|
+
_WEATHER_VARIANCE = {"low": 0.05, "normal": 0.15, "high": 0.25}
|
|
21
|
+
_NODE_VARIANCE = {"low": 0.10, "normal": 0.20, "high": 0.30}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_base_profile(hours: int, performance_ratio: float = 0.75) -> np.ndarray:
|
|
25
|
+
"""Generate a base bell-shaped solar profile.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
hours : int
|
|
30
|
+
Number of hours to simulate.
|
|
31
|
+
performance_ratio : float
|
|
32
|
+
System performance ratio (0-1).
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
np.ndarray
|
|
37
|
+
Base solar profile, shape (hours,).
|
|
38
|
+
"""
|
|
39
|
+
hours_mod = np.arange(hours) % 24
|
|
40
|
+
profile = np.zeros(hours)
|
|
41
|
+
|
|
42
|
+
daylight = (hours_mod >= 6) & (hours_mod <= 18)
|
|
43
|
+
profile[daylight] = np.sin(np.pi * (hours_mod[daylight] - 6) / 12)
|
|
44
|
+
|
|
45
|
+
return profile * performance_ratio
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def generate_cloud_pattern(
|
|
49
|
+
hours: int,
|
|
50
|
+
cloud_probability: float = 0.3,
|
|
51
|
+
rng: np.random.Generator | None = None,
|
|
52
|
+
) -> np.ndarray:
|
|
53
|
+
"""Generate stochastic cloud patterns.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
hours : int
|
|
58
|
+
Number of hours.
|
|
59
|
+
cloud_probability : float
|
|
60
|
+
Daily probability of cloud events.
|
|
61
|
+
rng : np.random.Generator or None
|
|
62
|
+
Random number generator.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
np.ndarray
|
|
67
|
+
Cloud attenuation pattern, shape (hours,). Values in [0, 1].
|
|
68
|
+
"""
|
|
69
|
+
if rng is None:
|
|
70
|
+
rng = np.random.default_rng()
|
|
71
|
+
|
|
72
|
+
num_days = max(1, hours // 24)
|
|
73
|
+
pattern = np.zeros(hours)
|
|
74
|
+
|
|
75
|
+
for day in range(num_days):
|
|
76
|
+
if rng.random() < cloud_probability:
|
|
77
|
+
cloud_start = rng.integers(6, 16)
|
|
78
|
+
cloud_duration = rng.integers(1, 4)
|
|
79
|
+
cloud_intensity = rng.uniform(0.3, 0.7)
|
|
80
|
+
day_offset = day * 24
|
|
81
|
+
for h in range(cloud_start, min(cloud_start + cloud_duration, 24)):
|
|
82
|
+
idx = day_offset + h
|
|
83
|
+
if idx < hours:
|
|
84
|
+
pattern[idx] = cloud_intensity
|
|
85
|
+
|
|
86
|
+
return pattern
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def generate_profiles(config: RooftopConfig) -> RooftopResult:
|
|
90
|
+
"""Generate stochastic rooftop solar availability profiles.
|
|
91
|
+
|
|
92
|
+
Creates hourly availability matrices with weather variability, cloud
|
|
93
|
+
patterns, and node-specific factors. Also computes adoption curves
|
|
94
|
+
and maximum potential.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
config : RooftopConfig
|
|
99
|
+
Configuration parameters.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
RooftopResult
|
|
104
|
+
Result containing availability matrix, adoption factors, and potentials.
|
|
105
|
+
"""
|
|
106
|
+
rng = np.random.default_rng(config.seed)
|
|
107
|
+
|
|
108
|
+
num_nodes = config.num_nodes
|
|
109
|
+
hours = config.hours
|
|
110
|
+
|
|
111
|
+
# Base solar profile
|
|
112
|
+
base_profile = generate_base_profile(hours, config.performance_ratio)
|
|
113
|
+
|
|
114
|
+
# Weather variability
|
|
115
|
+
weather_var = _WEATHER_VARIANCE.get(config.weather_variability, 0.15)
|
|
116
|
+
node_var = _NODE_VARIANCE.get(config.weather_variability, 0.20)
|
|
117
|
+
|
|
118
|
+
# Urbanization factors
|
|
119
|
+
urbanization = rng.beta(2, 2, num_nodes)
|
|
120
|
+
|
|
121
|
+
# Maximum potential
|
|
122
|
+
max_potential = calculate_potential_from_config(config, urbanization, rng)
|
|
123
|
+
|
|
124
|
+
# Daily weather component (shared across nodes)
|
|
125
|
+
num_days = max(1, hours // 24)
|
|
126
|
+
daily_weather = np.clip(
|
|
127
|
+
rng.normal(1.0, weather_var, num_days), 0.2, 1.8
|
|
128
|
+
)
|
|
129
|
+
daily_weather = np.repeat(daily_weather, 24)[:hours]
|
|
130
|
+
|
|
131
|
+
# Generate availability per node
|
|
132
|
+
availability = np.zeros((hours, num_nodes))
|
|
133
|
+
|
|
134
|
+
for node in range(num_nodes):
|
|
135
|
+
node_factor = float(np.clip(rng.normal(1.0, node_var), 0.6, 1.4))
|
|
136
|
+
hourly_noise = rng.normal(0, 0.05, hours)
|
|
137
|
+
cloud_pattern = generate_cloud_pattern(hours, rng=rng)
|
|
138
|
+
|
|
139
|
+
for h in range(hours):
|
|
140
|
+
raw = (
|
|
141
|
+
base_profile[h]
|
|
142
|
+
* node_factor
|
|
143
|
+
* daily_weather[h]
|
|
144
|
+
* (1 - cloud_pattern[h])
|
|
145
|
+
+ hourly_noise[h]
|
|
146
|
+
)
|
|
147
|
+
availability[h, node] = max(0.0, min(1.0, raw))
|
|
148
|
+
|
|
149
|
+
# Adoption factors
|
|
150
|
+
adoption = compute_adoption_curve(
|
|
151
|
+
num_nodes=num_nodes,
|
|
152
|
+
base_year=config.base_year,
|
|
153
|
+
target_year=config.target_year,
|
|
154
|
+
adoption_scenario=config.adoption_scenario,
|
|
155
|
+
initial_adoption=config.initial_adoption,
|
|
156
|
+
adoption_rates=config.adoption_rates,
|
|
157
|
+
max_adoption=config.max_adoption,
|
|
158
|
+
urbanization_factors=urbanization,
|
|
159
|
+
rng=rng,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return RooftopResult(
|
|
163
|
+
availability=availability,
|
|
164
|
+
adoption_factors=adoption,
|
|
165
|
+
max_potential_mw=max_potential,
|
|
166
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Cost learning curve and capacity integration for rooftop solar.
|
|
2
|
+
|
|
3
|
+
Models technology cost reduction over time and computes installed
|
|
4
|
+
capacity accounting for adoption dynamics and degradation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from rooftex.config import LearningCurveConfig
|
|
12
|
+
from rooftex.results import LearningCurveResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_learning_curve(
|
|
16
|
+
base_year: int,
|
|
17
|
+
target_year: int,
|
|
18
|
+
config: LearningCurveConfig | None = None,
|
|
19
|
+
years: list[int] | None = None,
|
|
20
|
+
) -> list[tuple[int, float]]:
|
|
21
|
+
"""Compute cost trajectory using exponential learning curve.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
base_year : int
|
|
26
|
+
Base year (cost = base_cost_per_kw).
|
|
27
|
+
target_year : int
|
|
28
|
+
Final year to compute.
|
|
29
|
+
config : LearningCurveConfig or None
|
|
30
|
+
Cost parameters. Uses defaults if None.
|
|
31
|
+
years : list[int] or None
|
|
32
|
+
Specific years to evaluate. If None, evaluates every year.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
list[tuple[int, float]]
|
|
37
|
+
List of (year, cost_per_kw) tuples.
|
|
38
|
+
"""
|
|
39
|
+
if config is None:
|
|
40
|
+
config = LearningCurveConfig()
|
|
41
|
+
|
|
42
|
+
if years is None:
|
|
43
|
+
years = list(range(base_year, target_year + 1))
|
|
44
|
+
|
|
45
|
+
result = []
|
|
46
|
+
for year in years:
|
|
47
|
+
years_diff = year - base_year
|
|
48
|
+
cost_factor = (1 - config.cost_reduction_rate) ** years_diff
|
|
49
|
+
cost = config.base_cost_per_kw * cost_factor
|
|
50
|
+
result.append((year, cost))
|
|
51
|
+
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def compute_installed_capacity(
|
|
56
|
+
year: int,
|
|
57
|
+
base_year: int,
|
|
58
|
+
target_year: int,
|
|
59
|
+
max_potential_mw: list[float],
|
|
60
|
+
adoption_factors: np.ndarray,
|
|
61
|
+
config: LearningCurveConfig | None = None,
|
|
62
|
+
) -> LearningCurveResult:
|
|
63
|
+
"""Compute installed capacity and cost at a specific year.
|
|
64
|
+
|
|
65
|
+
Applies S-curve progress factor and degradation to the adoption-based
|
|
66
|
+
capacity estimate.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
year : int
|
|
71
|
+
Year to evaluate.
|
|
72
|
+
base_year : int
|
|
73
|
+
Base year.
|
|
74
|
+
target_year : int
|
|
75
|
+
Target year for full adoption.
|
|
76
|
+
max_potential_mw : list[float]
|
|
77
|
+
Maximum potential per node in MW.
|
|
78
|
+
adoption_factors : np.ndarray
|
|
79
|
+
Adoption factors per node (from compute_adoption_curve).
|
|
80
|
+
config : LearningCurveConfig or None
|
|
81
|
+
Cost parameters. Uses defaults if None.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
LearningCurveResult
|
|
86
|
+
Installed capacity and cost information.
|
|
87
|
+
"""
|
|
88
|
+
if config is None:
|
|
89
|
+
config = LearningCurveConfig()
|
|
90
|
+
|
|
91
|
+
years_diff = year - base_year
|
|
92
|
+
|
|
93
|
+
# S-curve progress
|
|
94
|
+
progress = min(1.0, years_diff / max(1, target_year - base_year))
|
|
95
|
+
s_curve = 1.0 / (1.0 + np.exp(-10.0 * (progress - 0.5)))
|
|
96
|
+
|
|
97
|
+
current_adoption = adoption_factors * s_curve
|
|
98
|
+
|
|
99
|
+
# Installed capacity with degradation
|
|
100
|
+
installed = np.array(max_potential_mw) * current_adoption
|
|
101
|
+
degradation = 1.0 - (config.degradation_rate * years_diff / 2)
|
|
102
|
+
installed = installed * degradation
|
|
103
|
+
|
|
104
|
+
# Cost
|
|
105
|
+
cost_factor = (1 - config.cost_reduction_rate) ** years_diff
|
|
106
|
+
cost_per_kw = config.base_cost_per_kw * cost_factor
|
|
107
|
+
|
|
108
|
+
return LearningCurveResult(
|
|
109
|
+
year=year,
|
|
110
|
+
cost_per_kw=cost_per_kw,
|
|
111
|
+
cost_factor=cost_factor,
|
|
112
|
+
installed_capacity_mw=installed,
|
|
113
|
+
total_installed_mw=float(np.sum(installed)),
|
|
114
|
+
)
|
rooftex/results.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Result dataclasses for RoofteX."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RooftopResult:
|
|
13
|
+
"""Result of rooftop solar profile generation.
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
availability : np.ndarray
|
|
18
|
+
Hourly availability matrix, shape (hours, num_nodes). Values in [0, 1].
|
|
19
|
+
adoption_factors : np.ndarray
|
|
20
|
+
Adoption factor per node at the target year.
|
|
21
|
+
max_potential_mw : list[float]
|
|
22
|
+
Maximum rooftop PV potential per node in MW.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
availability: Any # np.ndarray (hours, num_nodes)
|
|
26
|
+
adoption_factors: Any # np.ndarray (num_nodes,)
|
|
27
|
+
max_potential_mw: list[float]
|
|
28
|
+
|
|
29
|
+
def compute_statistics(self) -> dict:
|
|
30
|
+
"""Compute summary statistics.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
dict
|
|
35
|
+
Dictionary with mean_cf, peak_cf, mean_adoption, total_potential_mw.
|
|
36
|
+
"""
|
|
37
|
+
avail = np.asarray(self.availability)
|
|
38
|
+
return {
|
|
39
|
+
"mean_cf": float(np.nanmean(avail[avail > 0])) if np.any(avail > 0) else 0.0,
|
|
40
|
+
"peak_cf": float(np.nanmax(avail)),
|
|
41
|
+
"mean_adoption": float(np.mean(self.adoption_factors)),
|
|
42
|
+
"total_potential_mw": sum(self.max_potential_mw),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
"""Convert result to dictionary."""
|
|
47
|
+
return {
|
|
48
|
+
"availability": np.asarray(self.availability).tolist(),
|
|
49
|
+
"adoption_factors": np.asarray(self.adoption_factors).tolist(),
|
|
50
|
+
"max_potential_mw": list(self.max_potential_mw),
|
|
51
|
+
"statistics": self.compute_statistics(),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class LearningCurveResult:
|
|
57
|
+
"""Result of cost learning curve computation.
|
|
58
|
+
|
|
59
|
+
Attributes
|
|
60
|
+
----------
|
|
61
|
+
year : int
|
|
62
|
+
Target year.
|
|
63
|
+
cost_per_kw : float
|
|
64
|
+
Projected cost in $/kW.
|
|
65
|
+
cost_factor : float
|
|
66
|
+
Cost reduction factor relative to base year.
|
|
67
|
+
installed_capacity_mw : np.ndarray
|
|
68
|
+
Installed capacity per node in MW.
|
|
69
|
+
total_installed_mw : float
|
|
70
|
+
Total installed capacity in MW.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
year: int
|
|
74
|
+
cost_per_kw: float
|
|
75
|
+
cost_factor: float
|
|
76
|
+
installed_capacity_mw: Any # np.ndarray
|
|
77
|
+
total_installed_mw: float
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rooftex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RoofteX - Rooftop Solar eXchange: rooftop PV potential, adoption dynamics, and profile generation
|
|
5
|
+
Author: RoofteX Development Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rooftex-dev/rooftex
|
|
8
|
+
Project-URL: Repository, https://github.com/rooftex-dev/rooftex
|
|
9
|
+
Keywords: rooftop solar,photovoltaic,renewable energy,adoption dynamics,solar profiles,distributed generation
|
|
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
|
+
Provides-Extra: viz
|
|
26
|
+
Requires-Dist: matplotlib>=3.4; extra == "viz"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: matplotlib>=3.4; extra == "all"
|
|
35
|
+
Requires-Dist: pytest>=7.0; extra == "all"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0; extra == "all"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# RoofteX — Rooftop Solar eXchange
|
|
40
|
+
|
|
41
|
+
[](https://github.com/msotocalvo/rooftex/actions/workflows/tests.yml)
|
|
42
|
+
[](https://doi.org/10.5281/zenodo.18898422)
|
|
43
|
+
|
|
44
|
+
A Python library for rooftop solar potential assessment, adoption dynamics modeling,
|
|
45
|
+
and stochastic availability profile generation.
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Rooftop Potential**: Estimate maximum rooftop PV capacity from population and dwelling data
|
|
50
|
+
- **Adoption Dynamics**: S-curve adoption modeling with urbanization and scenario parameters
|
|
51
|
+
- **Profile Generation**: Stochastic hourly solar profiles with cloud patterns and weather variability
|
|
52
|
+
- **Cost Learning Curve**: Technology cost projection with degradation and learning rates
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install rooftex
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from rooftex import RooftopConfig, generate_profiles, calculate_potential
|
|
64
|
+
|
|
65
|
+
# Estimate potential from population
|
|
66
|
+
potential = calculate_potential(population=[50000, 30000, 80000])
|
|
67
|
+
|
|
68
|
+
# Generate hourly availability profiles
|
|
69
|
+
config = RooftopConfig(
|
|
70
|
+
num_nodes=3,
|
|
71
|
+
hours=8760,
|
|
72
|
+
adoption_scenario="medium",
|
|
73
|
+
target_year=2040,
|
|
74
|
+
)
|
|
75
|
+
result = generate_profiles(config)
|
|
76
|
+
print(result.availability.shape) # (8760, 3)
|
|
77
|
+
print(result.adoption_factors.mean()) # ~0.3
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
rooftex/__init__.py,sha256=t0FkcHpWaSoG3_bsNLhcxy6a0Y7_UXAnnxPmgnOmtzI,990
|
|
2
|
+
rooftex/config.py,sha256=14pjAniBg3Vm8_nGe0EOi_ypHLw0gjoGjJKGTasZwBI,2497
|
|
3
|
+
rooftex/results.py,sha256=MzpI0s3I2DqxCDw5xzLOQ12Dk94HJkPQKWaP4tNorQM,2214
|
|
4
|
+
rooftex/core/__init__.py,sha256=hJhenDj7dPykWtVK2mCPcxaY_27zHvYUVvR9Q8W0O4U,272
|
|
5
|
+
rooftex/core/adoption.py,sha256=RrYxh1JDilGoz2KwOVui51XUN-WIBcbRkX9dMhmydko,3124
|
|
6
|
+
rooftex/core/potential.py,sha256=F-ZJcV91vyTNQk35aMZKYY4XWV7-wodIgr7m8fJvCgo,2982
|
|
7
|
+
rooftex/core/profiles.py,sha256=trtGGUVXszN4aleaPlcQVGCgTSig4W6E-as9gjIkdT8,4829
|
|
8
|
+
rooftex/economics/__init__.py,sha256=iF51f1qQwk-mO9pWLrMP4sCzV2QwHZGgQafXsuqrNVE,202
|
|
9
|
+
rooftex/economics/learning_curve.py,sha256=zdwnLnEFO8aZBARPj0bCa6IHvwUWaYishWyBe_8J91w,3238
|
|
10
|
+
rooftex-0.1.0.dist-info/licenses/LICENSE,sha256=1rBMQXlb7oUI5Swc58htPioIUQqMdqj5uhr7qK5sQTE,1081
|
|
11
|
+
rooftex-0.1.0.dist-info/METADATA,sha256=y9V43pFeCCFAH2__BobHlgbR3GjUfBoDo31FFlggzh4,2951
|
|
12
|
+
rooftex-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
13
|
+
rooftex-0.1.0.dist-info/top_level.txt,sha256=OTKhNYxgt1uBUJFT7ln4vEKrJFAm71ZTKgtvfmOYbD8,8
|
|
14
|
+
rooftex-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RoofteX 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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rooftex
|