rooftex 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.
rooftex-0.1.0/LICENSE ADDED
@@ -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.
rooftex-0.1.0/PKG-INFO ADDED
@@ -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
+ [![Tests](https://github.com/msotocalvo/rooftex/actions/workflows/tests.yml/badge.svg)](https://github.com/msotocalvo/rooftex/actions/workflows/tests.yml)
42
+ [![DOI](https://zenodo.org/badge/1175040710.svg)](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,44 @@
1
+ # RoofteX — Rooftop Solar eXchange
2
+
3
+ [![Tests](https://github.com/msotocalvo/rooftex/actions/workflows/tests.yml/badge.svg)](https://github.com/msotocalvo/rooftex/actions/workflows/tests.yml)
4
+ [![DOI](https://zenodo.org/badge/1175040710.svg)](https://doi.org/10.5281/zenodo.18898422)
5
+
6
+ A Python library for rooftop solar potential assessment, adoption dynamics modeling,
7
+ and stochastic availability profile generation.
8
+
9
+ ## Features
10
+
11
+ - **Rooftop Potential**: Estimate maximum rooftop PV capacity from population and dwelling data
12
+ - **Adoption Dynamics**: S-curve adoption modeling with urbanization and scenario parameters
13
+ - **Profile Generation**: Stochastic hourly solar profiles with cloud patterns and weather variability
14
+ - **Cost Learning Curve**: Technology cost projection with degradation and learning rates
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install rooftex
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```python
25
+ from rooftex import RooftopConfig, generate_profiles, calculate_potential
26
+
27
+ # Estimate potential from population
28
+ potential = calculate_potential(population=[50000, 30000, 80000])
29
+
30
+ # Generate hourly availability profiles
31
+ config = RooftopConfig(
32
+ num_nodes=3,
33
+ hours=8760,
34
+ adoption_scenario="medium",
35
+ target_year=2040,
36
+ )
37
+ result = generate_profiles(config)
38
+ print(result.availability.shape) # (8760, 3)
39
+ print(result.adoption_factors.mean()) # ~0.3
40
+ ```
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,84 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rooftex"
7
+ version = "0.1.0"
8
+ description = "RoofteX - Rooftop Solar eXchange: rooftop PV potential, adoption dynamics, and profile generation"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ {name = "RoofteX Development Team"},
14
+ ]
15
+ keywords = [
16
+ "rooftop solar",
17
+ "photovoltaic",
18
+ "renewable energy",
19
+ "adoption dynamics",
20
+ "solar profiles",
21
+ "distributed generation",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "Intended Audience :: Science/Research",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Operating System :: OS Independent",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Topic :: Scientific/Engineering",
34
+ "Topic :: Scientific/Engineering :: Physics",
35
+ ]
36
+
37
+ dependencies = [
38
+ "numpy>=1.22",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ viz = ["matplotlib>=3.4"]
43
+ dev = [
44
+ "pytest>=7.0",
45
+ "pytest-cov>=4.0",
46
+ "ruff>=0.1",
47
+ "build>=1.0",
48
+ "twine>=4.0",
49
+ ]
50
+ all = [
51
+ "matplotlib>=3.4",
52
+ "pytest>=7.0",
53
+ "pytest-cov>=4.0",
54
+ ]
55
+
56
+ [project.urls]
57
+ Homepage = "https://github.com/rooftex-dev/rooftex"
58
+ Repository = "https://github.com/rooftex-dev/rooftex"
59
+
60
+ [tool.setuptools.packages.find]
61
+ where = ["src"]
62
+ include = ["rooftex*"]
63
+ exclude = ["tests*"]
64
+
65
+ [tool.pytest.ini_options]
66
+ minversion = "7.0"
67
+ addopts = "-ra -q --strict-markers"
68
+ testpaths = ["tests"]
69
+ filterwarnings = [
70
+ "ignore::DeprecationWarning",
71
+ "ignore::UserWarning",
72
+ ]
73
+
74
+ [tool.ruff]
75
+ target-version = "py39"
76
+ line-length = 100
77
+
78
+ [tool.ruff.lint]
79
+ select = ["E", "W", "F", "I", "B", "C4", "UP"]
80
+ ignore = ["E501", "B008", "C901"]
81
+
82
+ [tool.ruff.lint.per-file-ignores]
83
+ "__init__.py" = ["F401"]
84
+ "tests/*" = ["B011"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -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
@@ -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
+ ]
@@ -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))
@@ -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,8 @@
1
+ """Economics modules for rooftop solar."""
2
+
3
+ from .learning_curve import compute_learning_curve, compute_installed_capacity
4
+
5
+ __all__ = [
6
+ "compute_learning_curve",
7
+ "compute_installed_capacity",
8
+ ]
@@ -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
+ )
@@ -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
+ [![Tests](https://github.com/msotocalvo/rooftex/actions/workflows/tests.yml/badge.svg)](https://github.com/msotocalvo/rooftex/actions/workflows/tests.yml)
42
+ [![DOI](https://zenodo.org/badge/1175040710.svg)](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,20 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/rooftex/__init__.py
5
+ src/rooftex/config.py
6
+ src/rooftex/results.py
7
+ src/rooftex.egg-info/PKG-INFO
8
+ src/rooftex.egg-info/SOURCES.txt
9
+ src/rooftex.egg-info/dependency_links.txt
10
+ src/rooftex.egg-info/requires.txt
11
+ src/rooftex.egg-info/top_level.txt
12
+ src/rooftex/core/__init__.py
13
+ src/rooftex/core/adoption.py
14
+ src/rooftex/core/potential.py
15
+ src/rooftex/core/profiles.py
16
+ src/rooftex/economics/__init__.py
17
+ src/rooftex/economics/learning_curve.py
18
+ tests/test_adoption.py
19
+ tests/test_potential.py
20
+ tests/test_profiles.py
@@ -0,0 +1,16 @@
1
+ numpy>=1.22
2
+
3
+ [all]
4
+ matplotlib>=3.4
5
+ pytest>=7.0
6
+ pytest-cov>=4.0
7
+
8
+ [dev]
9
+ pytest>=7.0
10
+ pytest-cov>=4.0
11
+ ruff>=0.1
12
+ build>=1.0
13
+ twine>=4.0
14
+
15
+ [viz]
16
+ matplotlib>=3.4
@@ -0,0 +1 @@
1
+ rooftex
@@ -0,0 +1,61 @@
1
+ """Tests for rooftex.core.adoption."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from rooftex.core.adoption import compute_adoption_curve
7
+
8
+
9
+ class TestAdoptionCurve:
10
+ def test_basic_shape(self):
11
+ result = compute_adoption_curve(
12
+ num_nodes=3,
13
+ base_year=2024,
14
+ target_year=2050,
15
+ rng=np.random.default_rng(42),
16
+ )
17
+ assert result.shape == (3,)
18
+ assert np.all(result >= 0)
19
+ assert np.all(result <= 1)
20
+
21
+ def test_higher_scenario_more_adoption(self):
22
+ rng_low = np.random.default_rng(42)
23
+ rng_high = np.random.default_rng(42)
24
+ low = compute_adoption_curve(
25
+ num_nodes=5, base_year=2024, target_year=2050,
26
+ adoption_scenario="low", rng=rng_low,
27
+ )
28
+ high = compute_adoption_curve(
29
+ num_nodes=5, base_year=2024, target_year=2050,
30
+ adoption_scenario="high", rng=rng_high,
31
+ )
32
+ assert np.mean(high) > np.mean(low)
33
+
34
+ def test_reproducibility(self):
35
+ a = compute_adoption_curve(
36
+ num_nodes=3, base_year=2024, target_year=2050,
37
+ rng=np.random.default_rng(99),
38
+ )
39
+ b = compute_adoption_curve(
40
+ num_nodes=3, base_year=2024, target_year=2050,
41
+ rng=np.random.default_rng(99),
42
+ )
43
+ np.testing.assert_array_equal(a, b)
44
+
45
+ def test_custom_initial_adoption(self):
46
+ result = compute_adoption_curve(
47
+ num_nodes=2, base_year=2024, target_year=2050,
48
+ initial_adoption=[0.1, 0.2],
49
+ rng=np.random.default_rng(42),
50
+ )
51
+ assert result.shape == (2,)
52
+
53
+ def test_custom_urbanization(self):
54
+ urban = np.array([0.9, 0.1])
55
+ result = compute_adoption_curve(
56
+ num_nodes=2, base_year=2024, target_year=2050,
57
+ urbanization_factors=urban,
58
+ rng=np.random.default_rng(42),
59
+ )
60
+ # Higher urbanization → higher adoption
61
+ assert result[0] > result[1]
@@ -0,0 +1,41 @@
1
+ """Tests for rooftex.core.potential."""
2
+
3
+ import pytest
4
+
5
+ from rooftex.core.potential import calculate_potential
6
+
7
+
8
+ class TestCalculatePotential:
9
+ def test_basic(self):
10
+ result = calculate_potential(population=[100000])
11
+ assert len(result) == 1
12
+ assert result[0] > 0
13
+
14
+ def test_multiple_nodes(self):
15
+ result = calculate_potential(population=[50000, 100000, 200000])
16
+ assert len(result) == 3
17
+ # Larger population → more potential
18
+ assert result[0] < result[1] < result[2]
19
+
20
+ def test_linear_scaling(self):
21
+ r1 = calculate_potential(population=[50000])
22
+ r2 = calculate_potential(population=[100000])
23
+ assert r2[0] == pytest.approx(r1[0] * 2, rel=1e-10)
24
+
25
+ def test_custom_params(self):
26
+ result = calculate_potential(
27
+ population=[100000],
28
+ dwelling_density=0.5,
29
+ avg_roof_area=40.0,
30
+ suitable_fraction=0.25,
31
+ panel_efficiency=0.22,
32
+ solar_irradiance=1000.0,
33
+ )
34
+ # 100000 * 0.5 = 50000 dwellings
35
+ # 50000 * 40 * 0.25 = 500000 m² suitable
36
+ # 500000 * 0.22 * 1000 / 1000 = 110000 kW = 110 MW
37
+ assert result[0] == pytest.approx(110.0)
38
+
39
+ def test_zero_population(self):
40
+ result = calculate_potential(population=[0])
41
+ assert result[0] == 0.0
@@ -0,0 +1,88 @@
1
+ """Tests for rooftex.core.profiles."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from rooftex.config import RooftopConfig
7
+ from rooftex.core.profiles import generate_base_profile, generate_cloud_pattern, generate_profiles
8
+
9
+
10
+ class TestBaseProfile:
11
+ def test_shape(self):
12
+ profile = generate_base_profile(8760)
13
+ assert profile.shape == (8760,)
14
+
15
+ def test_nighttime_zero(self):
16
+ profile = generate_base_profile(24)
17
+ # Hours 0-5 and 19-23 should be zero
18
+ assert profile[0] == 0.0
19
+ assert profile[3] == 0.0
20
+ assert profile[23] == 0.0
21
+
22
+ def test_peak_at_noon(self):
23
+ profile = generate_base_profile(24, performance_ratio=1.0)
24
+ assert profile[12] == pytest.approx(1.0, abs=0.01)
25
+
26
+ def test_performance_ratio(self):
27
+ p1 = generate_base_profile(24, performance_ratio=1.0)
28
+ p075 = generate_base_profile(24, performance_ratio=0.75)
29
+ np.testing.assert_allclose(p075, p1 * 0.75)
30
+
31
+
32
+ class TestCloudPattern:
33
+ def test_shape(self):
34
+ pattern = generate_cloud_pattern(8760, rng=np.random.default_rng(42))
35
+ assert pattern.shape == (8760,)
36
+
37
+ def test_values_range(self):
38
+ pattern = generate_cloud_pattern(8760, rng=np.random.default_rng(42))
39
+ assert np.all(pattern >= 0)
40
+ assert np.all(pattern < 1)
41
+
42
+ def test_zero_probability(self):
43
+ pattern = generate_cloud_pattern(8760, cloud_probability=0.0, rng=np.random.default_rng(42))
44
+ assert np.all(pattern == 0)
45
+
46
+
47
+ class TestGenerateProfiles:
48
+ def test_basic(self):
49
+ config = RooftopConfig(num_nodes=3, hours=48, seed=42)
50
+ result = generate_profiles(config)
51
+ assert result.availability.shape == (48, 3)
52
+ assert result.adoption_factors.shape == (3,)
53
+ assert len(result.max_potential_mw) == 3
54
+
55
+ def test_values_bounded(self):
56
+ config = RooftopConfig(num_nodes=2, hours=8760, seed=42)
57
+ result = generate_profiles(config)
58
+ assert np.all(result.availability >= 0)
59
+ assert np.all(result.availability <= 1)
60
+
61
+ def test_reproducibility(self):
62
+ config = RooftopConfig(num_nodes=2, hours=168, seed=99)
63
+ r1 = generate_profiles(config)
64
+ r2 = generate_profiles(config)
65
+ np.testing.assert_array_equal(r1.availability, r2.availability)
66
+
67
+ def test_statistics(self):
68
+ config = RooftopConfig(num_nodes=3, hours=8760, seed=42)
69
+ result = generate_profiles(config)
70
+ stats = result.compute_statistics()
71
+ assert "mean_cf" in stats
72
+ assert "peak_cf" in stats
73
+ assert stats["mean_cf"] > 0
74
+ assert stats["total_potential_mw"] > 0
75
+
76
+ def test_custom_systems(self):
77
+ config = RooftopConfig(
78
+ num_nodes=2,
79
+ hours=48,
80
+ seed=42,
81
+ systems_per_node=[1000, 2000],
82
+ avg_system_size_kw=[5.0, 4.0],
83
+ )
84
+ result = generate_profiles(config)
85
+ # Node 0: 1000 * 5.0 / 1000 = 5 MW
86
+ # Node 1: 2000 * 4.0 / 1000 = 8 MW
87
+ assert result.max_potential_mw[0] == pytest.approx(5.0)
88
+ assert result.max_potential_mw[1] == pytest.approx(8.0)