site-calc-investment 1.2.0__tar.gz → 1.2.1__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.
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/CHANGELOG.md +13 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/PKG-INFO +25 -17
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/QUICK_START.md +1 -1
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/README.md +24 -16
- site_calc_investment-1.2.1/examples/01_basic_capacity_planning.py +173 -0
- site_calc_investment-1.2.1/examples/02_scenario_comparison.py +210 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/pyproject.toml +1 -1
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/__init__.py +1 -1
- site_calc_investment-1.2.0/examples/01_basic_capacity_planning.py +0 -172
- site_calc_investment-1.2.0/examples/02_scenario_comparison.py +0 -197
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/.github/workflows/ci.yml +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/.github/workflows/publish.yml +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/.gitignore +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/CONTRIBUTING.md +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/LICENSE +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/MIGRATION_GUIDE.md +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/READY_TO_PUBLISH.md +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/docs/INVESTMENT_CLIENT_SPEC.md +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/examples/03_financial_analysis.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/publish_to_github.bat +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/publish_to_github.sh +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/__init__.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/comparison.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/financial.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/api/__init__.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/api/client.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/exceptions.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/__init__.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/common.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/devices.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/requests.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/responses.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/conftest.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_api_client.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_common_models.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_device_models.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_financial_analysis.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_production.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_request_models.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_scenario_comparison.py +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/uv.lock +0 -0
- {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/verify_ready.py +0 -0
|
@@ -5,6 +5,19 @@ All notable changes to the Site-Calc Investment Client will be documented in thi
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.1] - 2026-02-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **README Quick Start**: Fixed example code to use correct model classes and valid parameter values
|
|
12
|
+
- Use `TimeSpanInvestment` instead of `TimeSpan`
|
|
13
|
+
- Use valid `objective` values (`maximize_profit`, `minimize_cost`, `maximize_self_consumption`)
|
|
14
|
+
- Add required `project_lifetime_years` to `InvestmentParameters`
|
|
15
|
+
- Fix `time_limit_seconds` max value (900, not 3600)
|
|
16
|
+
- **Capabilities table**: Corrected timeout from "3600 seconds" to "900 seconds (15 minutes) max"
|
|
17
|
+
- **QUICK_START.md**: Added `pypi` environment name for Trusted Publishing setup
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
8
21
|
## [1.2.0] - 2026-02-03
|
|
9
22
|
|
|
10
23
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: site-calc-investment
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Python client for Site-Calc investment planning (capacity sizing, ROI analysis)
|
|
5
5
|
Project-URL: Homepage, https://github.com/stranma/site-calc-investment
|
|
6
6
|
Project-URL: Documentation, https://github.com/stranma/site-calc-investment#readme
|
|
@@ -52,9 +52,10 @@ from datetime import datetime
|
|
|
52
52
|
from zoneinfo import ZoneInfo
|
|
53
53
|
from site_calc_investment import InvestmentClient
|
|
54
54
|
from site_calc_investment.models import (
|
|
55
|
-
|
|
55
|
+
Resolution, Site, Battery, ElectricityImport, ElectricityExport,
|
|
56
56
|
InvestmentPlanningRequest, InvestmentParameters, OptimizationConfig
|
|
57
57
|
)
|
|
58
|
+
from site_calc_investment.models.requests import TimeSpanInvestment
|
|
58
59
|
|
|
59
60
|
# Initialize client
|
|
60
61
|
client = InvestmentClient(
|
|
@@ -62,13 +63,16 @@ client = InvestmentClient(
|
|
|
62
63
|
api_key="inv_your_api_key_here"
|
|
63
64
|
)
|
|
64
65
|
|
|
65
|
-
# Create
|
|
66
|
-
timespan =
|
|
66
|
+
# Create 1-week planning horizon (1-hour resolution)
|
|
67
|
+
timespan = TimeSpanInvestment(
|
|
67
68
|
start=datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Prague")),
|
|
68
|
-
intervals=
|
|
69
|
+
intervals=168, # 1 week = 7 days × 24 hours
|
|
69
70
|
resolution=Resolution.HOUR_1
|
|
70
71
|
)
|
|
71
72
|
|
|
73
|
+
# Generate hourly prices (example: day/night pattern)
|
|
74
|
+
prices = [30.0 if h % 24 < 6 else 80.0 if 8 <= h % 24 < 20 else 50.0 for h in range(168)]
|
|
75
|
+
|
|
72
76
|
# Define devices (NO ancillary_services field)
|
|
73
77
|
battery = Battery(
|
|
74
78
|
name="Battery1",
|
|
@@ -82,14 +86,20 @@ battery = Battery(
|
|
|
82
86
|
|
|
83
87
|
grid_import = ElectricityImport(
|
|
84
88
|
name="GridImport",
|
|
85
|
-
properties={"price":
|
|
89
|
+
properties={"price": prices, "max_import": 10.0}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
grid_export = ElectricityExport(
|
|
93
|
+
name="GridExport",
|
|
94
|
+
properties={"price": prices, "max_export": 10.0}
|
|
86
95
|
)
|
|
87
96
|
|
|
88
|
-
site = Site(site_id="investment_site", devices=[battery, grid_import])
|
|
97
|
+
site = Site(site_id="investment_site", devices=[battery, grid_import, grid_export])
|
|
89
98
|
|
|
90
99
|
# Investment parameters
|
|
91
100
|
inv_params = InvestmentParameters(
|
|
92
101
|
discount_rate=0.05,
|
|
102
|
+
project_lifetime_years=10, # Required field
|
|
93
103
|
device_capital_costs={"Battery1": 500000}, # €500k CAPEX
|
|
94
104
|
device_annual_opex={"Battery1": 5000} # €5k/year O&M
|
|
95
105
|
)
|
|
@@ -100,19 +110,17 @@ request = InvestmentPlanningRequest(
|
|
|
100
110
|
timespan=timespan,
|
|
101
111
|
investment_parameters=inv_params,
|
|
102
112
|
optimization_config=OptimizationConfig(
|
|
103
|
-
objective="
|
|
104
|
-
time_limit_seconds=
|
|
113
|
+
objective="maximize_profit", # Options: maximize_profit, minimize_cost, maximize_self_consumption
|
|
114
|
+
time_limit_seconds=300 # Max 900 seconds (15 min)
|
|
105
115
|
)
|
|
106
116
|
)
|
|
107
117
|
|
|
108
118
|
job = client.create_planning_job(request)
|
|
109
|
-
result = client.wait_for_completion(job.job_id, poll_interval=
|
|
119
|
+
result = client.wait_for_completion(job.job_id, poll_interval=5, timeout=600)
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
print(f"
|
|
114
|
-
print(f"IRR: {metrics.irr*100:.2f}%")
|
|
115
|
-
print(f"Payback: {metrics.payback_period_years:.1f} years")
|
|
121
|
+
print(f"Status: {result.status}")
|
|
122
|
+
print(f"Solver: {result.summary.solver_status}")
|
|
123
|
+
print(f"Profit: €{result.summary.expected_profit:,.2f}")
|
|
116
124
|
```
|
|
117
125
|
|
|
118
126
|
## Features
|
|
@@ -135,7 +143,7 @@ print(f"Payback: {metrics.payback_period_years:.1f} years")
|
|
|
135
143
|
| Resolution | 1-hour only |
|
|
136
144
|
| ANS Support | No |
|
|
137
145
|
| Binary Variables | Relaxed to continuous |
|
|
138
|
-
| Timeout |
|
|
146
|
+
| Timeout | 900 seconds (15 minutes) max |
|
|
139
147
|
|
|
140
148
|
## Supported Devices
|
|
141
149
|
|
|
@@ -203,7 +211,7 @@ print(comparison) # DataFrame with NPV, IRR, costs, revenues
|
|
|
203
211
|
|
|
204
212
|
## Documentation
|
|
205
213
|
|
|
206
|
-
Full documentation available at: https://
|
|
214
|
+
Full documentation available at: https://github.com/stranma/site-calc-investment#readme
|
|
207
215
|
|
|
208
216
|
## Examples
|
|
209
217
|
|
|
@@ -114,7 +114,7 @@ To enable `pip install site-calc-investment`:
|
|
|
114
114
|
- Owner: `YOUR-GITHUB-USERNAME`
|
|
115
115
|
- Repository name: `site-calc-investment`
|
|
116
116
|
- Workflow name: `publish.yml`
|
|
117
|
-
- Environment name:
|
|
117
|
+
- Environment name: `pypi`
|
|
118
118
|
|
|
119
119
|
3. **Done!** No API tokens needed (uses trusted publishing)
|
|
120
120
|
|
|
@@ -15,9 +15,10 @@ from datetime import datetime
|
|
|
15
15
|
from zoneinfo import ZoneInfo
|
|
16
16
|
from site_calc_investment import InvestmentClient
|
|
17
17
|
from site_calc_investment.models import (
|
|
18
|
-
|
|
18
|
+
Resolution, Site, Battery, ElectricityImport, ElectricityExport,
|
|
19
19
|
InvestmentPlanningRequest, InvestmentParameters, OptimizationConfig
|
|
20
20
|
)
|
|
21
|
+
from site_calc_investment.models.requests import TimeSpanInvestment
|
|
21
22
|
|
|
22
23
|
# Initialize client
|
|
23
24
|
client = InvestmentClient(
|
|
@@ -25,13 +26,16 @@ client = InvestmentClient(
|
|
|
25
26
|
api_key="inv_your_api_key_here"
|
|
26
27
|
)
|
|
27
28
|
|
|
28
|
-
# Create
|
|
29
|
-
timespan =
|
|
29
|
+
# Create 1-week planning horizon (1-hour resolution)
|
|
30
|
+
timespan = TimeSpanInvestment(
|
|
30
31
|
start=datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Prague")),
|
|
31
|
-
intervals=
|
|
32
|
+
intervals=168, # 1 week = 7 days × 24 hours
|
|
32
33
|
resolution=Resolution.HOUR_1
|
|
33
34
|
)
|
|
34
35
|
|
|
36
|
+
# Generate hourly prices (example: day/night pattern)
|
|
37
|
+
prices = [30.0 if h % 24 < 6 else 80.0 if 8 <= h % 24 < 20 else 50.0 for h in range(168)]
|
|
38
|
+
|
|
35
39
|
# Define devices (NO ancillary_services field)
|
|
36
40
|
battery = Battery(
|
|
37
41
|
name="Battery1",
|
|
@@ -45,14 +49,20 @@ battery = Battery(
|
|
|
45
49
|
|
|
46
50
|
grid_import = ElectricityImport(
|
|
47
51
|
name="GridImport",
|
|
48
|
-
properties={"price":
|
|
52
|
+
properties={"price": prices, "max_import": 10.0}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
grid_export = ElectricityExport(
|
|
56
|
+
name="GridExport",
|
|
57
|
+
properties={"price": prices, "max_export": 10.0}
|
|
49
58
|
)
|
|
50
59
|
|
|
51
|
-
site = Site(site_id="investment_site", devices=[battery, grid_import])
|
|
60
|
+
site = Site(site_id="investment_site", devices=[battery, grid_import, grid_export])
|
|
52
61
|
|
|
53
62
|
# Investment parameters
|
|
54
63
|
inv_params = InvestmentParameters(
|
|
55
64
|
discount_rate=0.05,
|
|
65
|
+
project_lifetime_years=10, # Required field
|
|
56
66
|
device_capital_costs={"Battery1": 500000}, # €500k CAPEX
|
|
57
67
|
device_annual_opex={"Battery1": 5000} # €5k/year O&M
|
|
58
68
|
)
|
|
@@ -63,19 +73,17 @@ request = InvestmentPlanningRequest(
|
|
|
63
73
|
timespan=timespan,
|
|
64
74
|
investment_parameters=inv_params,
|
|
65
75
|
optimization_config=OptimizationConfig(
|
|
66
|
-
objective="
|
|
67
|
-
time_limit_seconds=
|
|
76
|
+
objective="maximize_profit", # Options: maximize_profit, minimize_cost, maximize_self_consumption
|
|
77
|
+
time_limit_seconds=300 # Max 900 seconds (15 min)
|
|
68
78
|
)
|
|
69
79
|
)
|
|
70
80
|
|
|
71
81
|
job = client.create_planning_job(request)
|
|
72
|
-
result = client.wait_for_completion(job.job_id, poll_interval=
|
|
82
|
+
result = client.wait_for_completion(job.job_id, poll_interval=5, timeout=600)
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
print(f"
|
|
77
|
-
print(f"IRR: {metrics.irr*100:.2f}%")
|
|
78
|
-
print(f"Payback: {metrics.payback_period_years:.1f} years")
|
|
84
|
+
print(f"Status: {result.status}")
|
|
85
|
+
print(f"Solver: {result.summary.solver_status}")
|
|
86
|
+
print(f"Profit: €{result.summary.expected_profit:,.2f}")
|
|
79
87
|
```
|
|
80
88
|
|
|
81
89
|
## Features
|
|
@@ -98,7 +106,7 @@ print(f"Payback: {metrics.payback_period_years:.1f} years")
|
|
|
98
106
|
| Resolution | 1-hour only |
|
|
99
107
|
| ANS Support | No |
|
|
100
108
|
| Binary Variables | Relaxed to continuous |
|
|
101
|
-
| Timeout |
|
|
109
|
+
| Timeout | 900 seconds (15 minutes) max |
|
|
102
110
|
|
|
103
111
|
## Supported Devices
|
|
104
112
|
|
|
@@ -166,7 +174,7 @@ print(comparison) # DataFrame with NPV, IRR, costs, revenues
|
|
|
166
174
|
|
|
167
175
|
## Documentation
|
|
168
176
|
|
|
169
|
-
Full documentation available at: https://
|
|
177
|
+
Full documentation available at: https://github.com/stranma/site-calc-investment#readme
|
|
170
178
|
|
|
171
179
|
## Examples
|
|
172
180
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Basic Capacity Planning Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates a simple 1-week battery optimization
|
|
4
|
+
for capacity sizing and investment ROI analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from zoneinfo import ZoneInfo
|
|
10
|
+
|
|
11
|
+
from site_calc_investment import (
|
|
12
|
+
Battery,
|
|
13
|
+
ElectricityExport,
|
|
14
|
+
ElectricityImport,
|
|
15
|
+
InvestmentClient,
|
|
16
|
+
InvestmentParameters,
|
|
17
|
+
InvestmentPlanningRequest,
|
|
18
|
+
OptimizationConfig,
|
|
19
|
+
Site,
|
|
20
|
+
)
|
|
21
|
+
from site_calc_investment.models.requests import TimeSpanInvestment
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
# Get credentials from environment
|
|
26
|
+
api_url = os.environ.get("INVESTMENT_API_URL_DEV") or os.environ.get("INVESTMENT_API_URL")
|
|
27
|
+
api_key = os.environ.get("INVESTMENT_API_KEY_DEV") or os.environ.get("INVESTMENT_API_KEY")
|
|
28
|
+
|
|
29
|
+
if not api_url or not api_key:
|
|
30
|
+
print("ERROR: Set INVESTMENT_API_URL and INVESTMENT_API_KEY environment variables")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# Initialize client
|
|
34
|
+
client = InvestmentClient(
|
|
35
|
+
base_url=api_url,
|
|
36
|
+
api_key=api_key,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Create 1-week planning horizon (1-hour resolution)
|
|
40
|
+
timespan = TimeSpanInvestment(
|
|
41
|
+
start=datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Prague")),
|
|
42
|
+
intervals=168, # 1 week = 7 days x 24 hours
|
|
43
|
+
)
|
|
44
|
+
print(f"Planning horizon: {timespan.intervals} hourly intervals ({timespan.intervals / 24:.0f} days)")
|
|
45
|
+
|
|
46
|
+
# Generate price profile with day/night pattern
|
|
47
|
+
prices = []
|
|
48
|
+
for hour in range(168):
|
|
49
|
+
hour_of_day = hour % 24
|
|
50
|
+
if 9 <= hour_of_day <= 20:
|
|
51
|
+
prices.append(80.0) # Day: high price
|
|
52
|
+
else:
|
|
53
|
+
prices.append(30.0) # Night: low price
|
|
54
|
+
|
|
55
|
+
print(f"Price profile generated: {len(prices)} values")
|
|
56
|
+
print(" Day price: EUR 80/MWh, Night price: EUR 30/MWh")
|
|
57
|
+
|
|
58
|
+
# Define 10 MW / 20 MWh battery (2-hour duration)
|
|
59
|
+
battery = Battery(
|
|
60
|
+
name="Battery1",
|
|
61
|
+
properties={
|
|
62
|
+
"capacity": 20.0, # MWh
|
|
63
|
+
"max_power": 10.0, # MW (2-hour discharge)
|
|
64
|
+
"efficiency": 0.90, # 90% round-trip
|
|
65
|
+
"initial_soc": 0.5, # Start at 50%
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Market devices (grid connections)
|
|
70
|
+
grid_import = ElectricityImport(
|
|
71
|
+
name="GridImport",
|
|
72
|
+
properties={"price": prices, "max_import": 20.0},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
grid_export = ElectricityExport(
|
|
76
|
+
name="GridExport",
|
|
77
|
+
properties={"price": prices, "max_export": 20.0},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Create site
|
|
81
|
+
site = Site(
|
|
82
|
+
site_id="battery_investment_site",
|
|
83
|
+
description="Battery capacity planning example",
|
|
84
|
+
devices=[battery, grid_import, grid_export],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Investment parameters
|
|
88
|
+
inv_params = InvestmentParameters(
|
|
89
|
+
discount_rate=0.05, # 5% discount rate
|
|
90
|
+
project_lifetime_years=10, # Required field
|
|
91
|
+
device_capital_costs={
|
|
92
|
+
"Battery1": 2_000_000 # EUR 2M CAPEX (EUR 100/kWh)
|
|
93
|
+
},
|
|
94
|
+
device_annual_opex={
|
|
95
|
+
"Battery1": 20_000 # EUR 20k/year O&M
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Create optimization request
|
|
100
|
+
request = InvestmentPlanningRequest(
|
|
101
|
+
sites=[site],
|
|
102
|
+
timespan=timespan,
|
|
103
|
+
investment_parameters=inv_params,
|
|
104
|
+
optimization_config=OptimizationConfig(
|
|
105
|
+
objective="maximize_profit", # Options: maximize_profit, minimize_cost, maximize_self_consumption
|
|
106
|
+
time_limit_seconds=300, # 5 minutes max
|
|
107
|
+
relax_binary_variables=True,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
print("\n" + "=" * 60)
|
|
112
|
+
print("Submitting optimization job...")
|
|
113
|
+
print("=" * 60)
|
|
114
|
+
|
|
115
|
+
# Submit job
|
|
116
|
+
job = client.create_planning_job(request)
|
|
117
|
+
print(f"\nJob ID: {job.job_id}")
|
|
118
|
+
print(f"Status: {job.status}")
|
|
119
|
+
|
|
120
|
+
# Wait for completion
|
|
121
|
+
print("\nWaiting for optimization to complete...")
|
|
122
|
+
|
|
123
|
+
result = client.wait_for_completion(
|
|
124
|
+
job.job_id,
|
|
125
|
+
poll_interval=5, # Check every 5 seconds
|
|
126
|
+
timeout=600, # 10 minute maximum
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
print("\n" + "=" * 60)
|
|
130
|
+
print("OPTIMIZATION RESULTS")
|
|
131
|
+
print("=" * 60)
|
|
132
|
+
|
|
133
|
+
# Summary
|
|
134
|
+
summary = result.summary
|
|
135
|
+
print(f"\nSolver Status: {summary.solver_status}")
|
|
136
|
+
print(f"Solve Time: {summary.solve_time_seconds:.1f}s")
|
|
137
|
+
|
|
138
|
+
if summary.expected_profit is not None:
|
|
139
|
+
print(f"Expected Profit: EUR {summary.expected_profit:,.2f}")
|
|
140
|
+
|
|
141
|
+
# Investment metrics (if available)
|
|
142
|
+
if result.investment_metrics:
|
|
143
|
+
metrics = result.investment_metrics
|
|
144
|
+
print("\nINVESTMENT METRICS:")
|
|
145
|
+
if metrics.total_revenue_10y is not None:
|
|
146
|
+
print(f" Total Revenue (10y): EUR {metrics.total_revenue_10y:>15,.0f}")
|
|
147
|
+
if metrics.total_costs_10y is not None:
|
|
148
|
+
print(f" Total Costs (10y): EUR {metrics.total_costs_10y:>15,.0f}")
|
|
149
|
+
if metrics.npv is not None:
|
|
150
|
+
print(f" NPV: EUR {metrics.npv:>15,.0f}")
|
|
151
|
+
if metrics.irr is not None:
|
|
152
|
+
print(f" IRR: {metrics.irr * 100:>15.2f}%")
|
|
153
|
+
if metrics.payback_period_years is not None:
|
|
154
|
+
print(f" Payback Period: {metrics.payback_period_years:>15.1f} years")
|
|
155
|
+
|
|
156
|
+
# Device schedules (first 24 hours)
|
|
157
|
+
site_result = result.sites.get("battery_investment_site")
|
|
158
|
+
if site_result:
|
|
159
|
+
battery_schedule = site_result.device_schedules.get("Battery1")
|
|
160
|
+
if battery_schedule and battery_schedule.flows.get("electricity"):
|
|
161
|
+
print("\nBATTERY OPERATION (First 24 hours):")
|
|
162
|
+
el_flow = battery_schedule.flows["electricity"]
|
|
163
|
+
soc = battery_schedule.soc or []
|
|
164
|
+
|
|
165
|
+
for hour in range(min(24, len(el_flow))):
|
|
166
|
+
soc_str = f"{soc[hour]:.1%}" if hour < len(soc) else "N/A"
|
|
167
|
+
print(f" Hour {hour:2d}: Power {el_flow[hour]:>7.2f} MW | SOC {soc_str}")
|
|
168
|
+
|
|
169
|
+
print("\n" + "=" * 60)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Scenario Comparison Example
|
|
2
|
+
|
|
3
|
+
This example compares three different battery sizes to find the optimal
|
|
4
|
+
capacity for investment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from zoneinfo import ZoneInfo
|
|
10
|
+
|
|
11
|
+
from site_calc_investment import (
|
|
12
|
+
Battery,
|
|
13
|
+
ElectricityExport,
|
|
14
|
+
ElectricityImport,
|
|
15
|
+
InvestmentClient,
|
|
16
|
+
InvestmentParameters,
|
|
17
|
+
InvestmentPlanningRequest,
|
|
18
|
+
OptimizationConfig,
|
|
19
|
+
Site,
|
|
20
|
+
compare_scenarios,
|
|
21
|
+
)
|
|
22
|
+
from site_calc_investment.models.requests import TimeSpanInvestment
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_prices(days: int = 7):
|
|
26
|
+
"""Create price profile with daily pattern."""
|
|
27
|
+
prices = []
|
|
28
|
+
for day in range(days):
|
|
29
|
+
for hour in range(24):
|
|
30
|
+
if 9 <= hour <= 20:
|
|
31
|
+
prices.append(80.0) # Day: high price
|
|
32
|
+
else:
|
|
33
|
+
prices.append(30.0) # Night: low price
|
|
34
|
+
return prices
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_scenario(
|
|
38
|
+
client: InvestmentClient,
|
|
39
|
+
capacity_mwh: float,
|
|
40
|
+
prices: list,
|
|
41
|
+
timespan: TimeSpanInvestment,
|
|
42
|
+
) -> tuple:
|
|
43
|
+
"""Create and run a scenario with given battery capacity.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
(scenario_name, result)
|
|
47
|
+
"""
|
|
48
|
+
scenario_name = f"{capacity_mwh:.0f} MWh Battery"
|
|
49
|
+
print(f"\n{'=' * 60}")
|
|
50
|
+
print(f"SCENARIO: {scenario_name}")
|
|
51
|
+
print(f"{'=' * 60}")
|
|
52
|
+
|
|
53
|
+
# Battery sized for 2-hour duration
|
|
54
|
+
battery = Battery(
|
|
55
|
+
name="Battery1",
|
|
56
|
+
properties={
|
|
57
|
+
"capacity": capacity_mwh,
|
|
58
|
+
"max_power": capacity_mwh / 2, # 2-hour discharge
|
|
59
|
+
"efficiency": 0.90,
|
|
60
|
+
"initial_soc": 0.5,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
grid_import = ElectricityImport(
|
|
65
|
+
name="GridImport",
|
|
66
|
+
properties={"price": prices, "max_import": 50.0},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
grid_export = ElectricityExport(
|
|
70
|
+
name="GridExport",
|
|
71
|
+
properties={"price": prices, "max_export": 50.0},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
site = Site(
|
|
75
|
+
site_id=f"site_{capacity_mwh:.0f}mwh",
|
|
76
|
+
devices=[battery, grid_import, grid_export],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Capital cost: EUR 100/kWh
|
|
80
|
+
capex = capacity_mwh * 1000 * 100 # EUR 100/kWh
|
|
81
|
+
|
|
82
|
+
# O&M: EUR 1/kWh/year
|
|
83
|
+
opex = capacity_mwh * 1000 * 1 # EUR 1/kWh/year
|
|
84
|
+
|
|
85
|
+
inv_params = InvestmentParameters(
|
|
86
|
+
discount_rate=0.05,
|
|
87
|
+
project_lifetime_years=10, # Required field
|
|
88
|
+
device_capital_costs={"Battery1": capex},
|
|
89
|
+
device_annual_opex={"Battery1": opex},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
request = InvestmentPlanningRequest(
|
|
93
|
+
sites=[site],
|
|
94
|
+
timespan=timespan,
|
|
95
|
+
investment_parameters=inv_params,
|
|
96
|
+
optimization_config=OptimizationConfig(
|
|
97
|
+
objective="maximize_profit", # Valid options: maximize_profit, minimize_cost, maximize_self_consumption
|
|
98
|
+
time_limit_seconds=300,
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
print(f" Capacity: {capacity_mwh:.0f} MWh")
|
|
103
|
+
print(f" Power: {capacity_mwh / 2:.0f} MW")
|
|
104
|
+
print(f" CAPEX: EUR {capex:,.0f}")
|
|
105
|
+
print(f" Annual O&M: EUR {opex:,.0f}")
|
|
106
|
+
|
|
107
|
+
job = client.create_planning_job(request)
|
|
108
|
+
print(f"\n Job ID: {job.job_id}")
|
|
109
|
+
print(" Waiting for completion...")
|
|
110
|
+
|
|
111
|
+
result = client.wait_for_completion(job.job_id, poll_interval=5, timeout=600)
|
|
112
|
+
|
|
113
|
+
print(f" Completed in {result.summary.solve_time_seconds:.0f}s")
|
|
114
|
+
|
|
115
|
+
if result.investment_metrics:
|
|
116
|
+
metrics = result.investment_metrics
|
|
117
|
+
npv_str = f"EUR {metrics.npv:,.0f}" if metrics.npv else "N/A"
|
|
118
|
+
irr_str = f"{metrics.irr * 100:.2f}%" if metrics.irr else "N/A"
|
|
119
|
+
payback_str = f"{metrics.payback_period_years:.1f} years" if metrics.payback_period_years else "N/A"
|
|
120
|
+
print(f"\n NPV: {npv_str}")
|
|
121
|
+
print(f" IRR: {irr_str}")
|
|
122
|
+
print(f" Payback: {payback_str}")
|
|
123
|
+
|
|
124
|
+
return scenario_name, result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main():
|
|
128
|
+
print("=" * 60)
|
|
129
|
+
print("BATTERY CAPACITY SIZING: SCENARIO COMPARISON")
|
|
130
|
+
print("=" * 60)
|
|
131
|
+
print("\nComparing three battery sizes over 1-week horizon")
|
|
132
|
+
print("Goal: Find optimal capacity for maximum profit")
|
|
133
|
+
|
|
134
|
+
# Get credentials from environment
|
|
135
|
+
api_url = os.environ.get("INVESTMENT_API_URL_DEV") or os.environ.get("INVESTMENT_API_URL")
|
|
136
|
+
api_key = os.environ.get("INVESTMENT_API_KEY_DEV") or os.environ.get("INVESTMENT_API_KEY")
|
|
137
|
+
|
|
138
|
+
if not api_url or not api_key:
|
|
139
|
+
print("\nERROR: Set INVESTMENT_API_URL and INVESTMENT_API_KEY environment variables")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# Initialize client
|
|
143
|
+
client = InvestmentClient(
|
|
144
|
+
base_url=api_url,
|
|
145
|
+
api_key=api_key,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# 1-week planning
|
|
149
|
+
timespan = TimeSpanInvestment(
|
|
150
|
+
start=datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Prague")),
|
|
151
|
+
intervals=168, # 1 week
|
|
152
|
+
)
|
|
153
|
+
prices = create_prices(days=7)
|
|
154
|
+
|
|
155
|
+
print(f"\nPrices: {len(prices)} hourly values")
|
|
156
|
+
print(" Day price: EUR 80/MWh, Night price: EUR 30/MWh")
|
|
157
|
+
|
|
158
|
+
# Test three capacities
|
|
159
|
+
capacities = [10.0, 20.0, 30.0] # MWh
|
|
160
|
+
|
|
161
|
+
scenarios = []
|
|
162
|
+
for capacity in capacities:
|
|
163
|
+
name, result = create_scenario(client, capacity, prices, timespan)
|
|
164
|
+
scenarios.append((name, result))
|
|
165
|
+
|
|
166
|
+
# Compare scenarios
|
|
167
|
+
print("\n" + "=" * 60)
|
|
168
|
+
print("SCENARIO COMPARISON")
|
|
169
|
+
print("=" * 60)
|
|
170
|
+
|
|
171
|
+
names = [s[0] for s in scenarios]
|
|
172
|
+
results = [s[1] for s in scenarios]
|
|
173
|
+
|
|
174
|
+
comparison = compare_scenarios(results, names=names)
|
|
175
|
+
|
|
176
|
+
# Print comparison table
|
|
177
|
+
print(f"\n{'Scenario':<20} {'Profit':>15} {'NPV':>15} {'IRR':>10}")
|
|
178
|
+
print("-" * 65)
|
|
179
|
+
|
|
180
|
+
for i, name in enumerate(comparison["names"]):
|
|
181
|
+
profit = comparison.get("profit", [None] * len(names))[i]
|
|
182
|
+
npv = comparison.get("npv", [None] * len(names))[i]
|
|
183
|
+
irr = comparison.get("irr", [None] * len(names))[i]
|
|
184
|
+
|
|
185
|
+
profit_str = f"EUR {profit:,.0f}" if profit is not None else "N/A"
|
|
186
|
+
npv_str = f"EUR {npv:,.0f}" if npv is not None else "N/A"
|
|
187
|
+
irr_str = f"{irr * 100:.2f}%" if irr is not None else "N/A"
|
|
188
|
+
|
|
189
|
+
print(f"{name:<20} {profit_str:>15} {npv_str:>15} {irr_str:>10}")
|
|
190
|
+
|
|
191
|
+
# Find optimal by profit
|
|
192
|
+
profit_values = comparison.get("profit", [])
|
|
193
|
+
if profit_values:
|
|
194
|
+
valid_profits = [(i, v) for i, v in enumerate(profit_values) if v is not None]
|
|
195
|
+
if valid_profits:
|
|
196
|
+
best_idx, best_profit = max(valid_profits, key=lambda x: x[1])
|
|
197
|
+
best_name = comparison["names"][best_idx]
|
|
198
|
+
|
|
199
|
+
print("\n" + "=" * 60)
|
|
200
|
+
print(f"OPTIMAL CONFIGURATION: {best_name}")
|
|
201
|
+
print("=" * 60)
|
|
202
|
+
print(f" Profit: EUR {best_profit:,.0f}")
|
|
203
|
+
else:
|
|
204
|
+
print("\nNo profit data available for comparison")
|
|
205
|
+
|
|
206
|
+
print("=" * 60)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
"""Basic Capacity Planning Example
|
|
2
|
-
|
|
3
|
-
This example demonstrates a simple 10-year battery optimization
|
|
4
|
-
for capacity sizing and investment ROI analysis.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from site_calc_investment import (
|
|
8
|
-
Battery,
|
|
9
|
-
BatteryProperties,
|
|
10
|
-
ElectricityExport,
|
|
11
|
-
ElectricityImport,
|
|
12
|
-
InvestmentClient,
|
|
13
|
-
InvestmentParameters,
|
|
14
|
-
InvestmentPlanningRequest,
|
|
15
|
-
MarketExportProperties,
|
|
16
|
-
MarketImportProperties,
|
|
17
|
-
OptimizationConfig,
|
|
18
|
-
Site,
|
|
19
|
-
TimeSpan,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def main():
|
|
24
|
-
# Initialize client
|
|
25
|
-
client = InvestmentClient(
|
|
26
|
-
base_url="https://api.site-calc.example.com",
|
|
27
|
-
api_key="inv_your_api_key_here",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
# Create 10-year planning horizon (1-hour resolution)
|
|
31
|
-
timespan = TimeSpan.for_years(start_year=2025, years=10)
|
|
32
|
-
print(f"Planning horizon: {timespan.years} years ({timespan.intervals} hourly intervals)")
|
|
33
|
-
|
|
34
|
-
# Generate simple price profile with 2% annual escalation
|
|
35
|
-
base_hourly_prices = []
|
|
36
|
-
for hour in range(24):
|
|
37
|
-
# Higher prices during day (peak)
|
|
38
|
-
if 9 <= hour <= 20:
|
|
39
|
-
price = 40.0
|
|
40
|
-
else:
|
|
41
|
-
price = 25.0
|
|
42
|
-
base_hourly_prices.append(price)
|
|
43
|
-
|
|
44
|
-
# Extend to 10 years with 2% annual escalation
|
|
45
|
-
prices_10y = []
|
|
46
|
-
for year in range(10):
|
|
47
|
-
escalation_factor = 1.02**year
|
|
48
|
-
year_prices = [p * escalation_factor for p in base_hourly_prices] * 365
|
|
49
|
-
prices_10y.extend(year_prices)
|
|
50
|
-
|
|
51
|
-
print(f"Price profile generated: {len(prices_10y)} values")
|
|
52
|
-
print(f" Year 1 avg: €{sum(prices_10y[:8760]) / 8760:.2f}/MWh")
|
|
53
|
-
print(f" Year 10 avg: €{sum(prices_10y[-8760:]) / 8760:.2f}/MWh")
|
|
54
|
-
|
|
55
|
-
# Define 10 MW / 20 MWh battery (2-hour duration)
|
|
56
|
-
battery = Battery(
|
|
57
|
-
name="Battery1",
|
|
58
|
-
properties=BatteryProperties(
|
|
59
|
-
capacity=20.0, # MWh
|
|
60
|
-
max_power=10.0, # MW (2-hour discharge)
|
|
61
|
-
efficiency=0.90, # 90% round-trip
|
|
62
|
-
initial_soc=0.5, # Start at 50%
|
|
63
|
-
),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# Market devices (grid connections)
|
|
67
|
-
grid_import = ElectricityImport(
|
|
68
|
-
name="GridImport", properties=MarketImportProperties(price=prices_10y, max_import=20.0)
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
grid_export = ElectricityExport(
|
|
72
|
-
name="GridExport", properties=MarketExportProperties(price=prices_10y, max_export=20.0)
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
# Create site
|
|
76
|
-
site = Site(
|
|
77
|
-
site_id="battery_investment_site",
|
|
78
|
-
description="10-year battery capacity planning",
|
|
79
|
-
devices=[battery, grid_import, grid_export],
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Investment parameters
|
|
83
|
-
inv_params = InvestmentParameters(
|
|
84
|
-
discount_rate=0.05, # 5% discount rate
|
|
85
|
-
device_capital_costs={
|
|
86
|
-
"Battery1": 2_000_000 # €2M CAPEX (€100/kWh)
|
|
87
|
-
},
|
|
88
|
-
device_annual_opex={
|
|
89
|
-
"Battery1": 20_000 # €20k/year O&M
|
|
90
|
-
},
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
# Create optimization request
|
|
94
|
-
request = InvestmentPlanningRequest(
|
|
95
|
-
sites=[site],
|
|
96
|
-
timespan=timespan,
|
|
97
|
-
investment_parameters=inv_params,
|
|
98
|
-
optimization_config=OptimizationConfig(
|
|
99
|
-
objective="maximize_npv",
|
|
100
|
-
time_limit_seconds=300, # 5 minutes
|
|
101
|
-
relax_binary_variables=True,
|
|
102
|
-
),
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
print("\n" + "=" * 80)
|
|
106
|
-
print("Submitting optimization job...")
|
|
107
|
-
print("=" * 80)
|
|
108
|
-
|
|
109
|
-
# Submit job
|
|
110
|
-
job = client.create_planning_job(request)
|
|
111
|
-
print(f"\nJob ID: {job.job_id}")
|
|
112
|
-
print(f"Status: {job.status}")
|
|
113
|
-
|
|
114
|
-
# Wait for completion (with progress updates)
|
|
115
|
-
print("\nWaiting for optimization to complete...")
|
|
116
|
-
print("(This may take 15-60 minutes for 10-year horizon)")
|
|
117
|
-
|
|
118
|
-
result = client.wait_for_completion(
|
|
119
|
-
job.job_id,
|
|
120
|
-
poll_interval=30, # Check every 30 seconds
|
|
121
|
-
timeout=7200, # 2 hour maximum
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
print("\n" + "=" * 80)
|
|
125
|
-
print("OPTIMIZATION RESULTS")
|
|
126
|
-
print("=" * 80)
|
|
127
|
-
|
|
128
|
-
# Summary
|
|
129
|
-
summary = result.summary
|
|
130
|
-
print(f"\nSolver Status: {summary.solver_status}")
|
|
131
|
-
print(f"Solve Time: {summary.solve_time_seconds:.1f}s ({summary.solve_time_seconds / 60:.1f} min)")
|
|
132
|
-
|
|
133
|
-
# Financial metrics
|
|
134
|
-
if summary.investment_metrics:
|
|
135
|
-
metrics = summary.investment_metrics
|
|
136
|
-
print("\nFINANCIAL METRICS:")
|
|
137
|
-
print(f" Total Revenue (10y): €{metrics.total_revenue_period:>15,.0f}")
|
|
138
|
-
print(f" Total Costs (10y): €{metrics.total_costs_period:>15,.0f}")
|
|
139
|
-
print(f" Net Profit (10y): €{summary.expected_profit:>15,.0f}")
|
|
140
|
-
print(f"\n NPV: €{metrics.npv:>15,.0f}")
|
|
141
|
-
print(f" IRR: {metrics.irr * 100:>15.2f}%")
|
|
142
|
-
print(f" Payback Period: {metrics.payback_period_years:>15.1f} years")
|
|
143
|
-
|
|
144
|
-
# Annual breakdown
|
|
145
|
-
if metrics.annual_revenue_by_year:
|
|
146
|
-
print("\nANNUAL BREAKDOWN:")
|
|
147
|
-
revenues = metrics.annual_revenue_by_year
|
|
148
|
-
costs = metrics.annual_costs_by_year
|
|
149
|
-
for year, (revenue, cost) in enumerate(zip(revenues, costs), 1):
|
|
150
|
-
print(f" Year {year:2d}: Revenue €{revenue:>10,.0f} | Cost €{cost:>10,.0f}")
|
|
151
|
-
|
|
152
|
-
# Device schedules (first and last 24 hours)
|
|
153
|
-
site_result = result.sites["battery_investment_site"]
|
|
154
|
-
battery_schedule = site_result.device_schedules["Battery1"]
|
|
155
|
-
|
|
156
|
-
print("\nBATTERY OPERATION (First 24 hours):")
|
|
157
|
-
el_flow = battery_schedule.flows["electricity"]
|
|
158
|
-
soc = battery_schedule.soc
|
|
159
|
-
|
|
160
|
-
for hour in range(24):
|
|
161
|
-
print(f" Hour {hour:2d}: Power {el_flow[hour]:>6.2f} MW | SOC {soc[hour]:>5.1%}")
|
|
162
|
-
|
|
163
|
-
print("\nBATTERY OPERATION (Last 24 hours):")
|
|
164
|
-
for hour in range(-24, 0):
|
|
165
|
-
actual_hour = hour % 24
|
|
166
|
-
print(f" Hour {actual_hour:2d}: Power {el_flow[hour]:>6.2f} MW | SOC {soc[hour]:>5.1%}")
|
|
167
|
-
|
|
168
|
-
print("\n" + "=" * 80)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if __name__ == "__main__":
|
|
172
|
-
main()
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
"""Scenario Comparison Example
|
|
2
|
-
|
|
3
|
-
This example compares three different battery sizes to find the optimal
|
|
4
|
-
capacity for a 10-year investment.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from site_calc_investment import (
|
|
8
|
-
Battery,
|
|
9
|
-
BatteryProperties,
|
|
10
|
-
ElectricityExport,
|
|
11
|
-
ElectricityImport,
|
|
12
|
-
InvestmentClient,
|
|
13
|
-
InvestmentParameters,
|
|
14
|
-
InvestmentPlanningRequest,
|
|
15
|
-
MarketExportProperties,
|
|
16
|
-
MarketImportProperties,
|
|
17
|
-
OptimizationConfig,
|
|
18
|
-
Site,
|
|
19
|
-
TimeSpan,
|
|
20
|
-
compare_scenarios,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def create_prices(years: int = 10, escalation_rate: float = 0.02):
|
|
25
|
-
"""Create price profile with daily pattern and annual escalation."""
|
|
26
|
-
base_hourly = []
|
|
27
|
-
for hour in range(24):
|
|
28
|
-
if 9 <= hour <= 20:
|
|
29
|
-
price = 40.0
|
|
30
|
-
else:
|
|
31
|
-
price = 25.0
|
|
32
|
-
base_hourly.append(price)
|
|
33
|
-
|
|
34
|
-
prices = []
|
|
35
|
-
for year in range(years):
|
|
36
|
-
factor = (1 + escalation_rate) ** year
|
|
37
|
-
prices.extend([p * factor for p in base_hourly] * 365)
|
|
38
|
-
|
|
39
|
-
return prices
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def create_scenario(
|
|
43
|
-
client: InvestmentClient,
|
|
44
|
-
capacity_mwh: float,
|
|
45
|
-
prices: list,
|
|
46
|
-
timespan: TimeSpan,
|
|
47
|
-
) -> tuple:
|
|
48
|
-
"""Create and run a scenario with given battery capacity.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
(scenario_name, result)
|
|
52
|
-
"""
|
|
53
|
-
scenario_name = f"{capacity_mwh:.0f} MWh Battery"
|
|
54
|
-
print(f"\n{'=' * 80}")
|
|
55
|
-
print(f"SCENARIO: {scenario_name}")
|
|
56
|
-
print(f"{'=' * 80}")
|
|
57
|
-
|
|
58
|
-
# Battery sized for 2-hour duration
|
|
59
|
-
battery = Battery(
|
|
60
|
-
name="Battery1",
|
|
61
|
-
properties=BatteryProperties(
|
|
62
|
-
capacity=capacity_mwh,
|
|
63
|
-
max_power=capacity_mwh / 2, # 2-hour discharge
|
|
64
|
-
efficiency=0.90,
|
|
65
|
-
initial_soc=0.5,
|
|
66
|
-
),
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
grid_import = ElectricityImport(name="GridImport", properties=MarketImportProperties(price=prices, max_import=50.0))
|
|
70
|
-
|
|
71
|
-
grid_export = ElectricityExport(name="GridExport", properties=MarketExportProperties(price=prices, max_export=50.0))
|
|
72
|
-
|
|
73
|
-
site = Site(site_id=f"site_{capacity_mwh:.0f}mwh", devices=[battery, grid_import, grid_export])
|
|
74
|
-
|
|
75
|
-
# Capital cost: €100/kWh
|
|
76
|
-
capex = capacity_mwh * 1000 * 100 # €100/kWh
|
|
77
|
-
|
|
78
|
-
# O&M: €1/kWh/year
|
|
79
|
-
opex = capacity_mwh * 1000 * 1 # €1/kWh/year
|
|
80
|
-
|
|
81
|
-
inv_params = InvestmentParameters(
|
|
82
|
-
discount_rate=0.05, device_capital_costs={"Battery1": capex}, device_annual_opex={"Battery1": opex}
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
request = InvestmentPlanningRequest(
|
|
86
|
-
sites=[site],
|
|
87
|
-
timespan=timespan,
|
|
88
|
-
investment_parameters=inv_params,
|
|
89
|
-
optimization_config=OptimizationConfig(objective="maximize_npv", time_limit_seconds=300),
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
print(f" Capacity: {capacity_mwh:.0f} MWh")
|
|
93
|
-
print(f" Power: {capacity_mwh / 2:.0f} MW")
|
|
94
|
-
print(f" CAPEX: €{capex:,.0f}")
|
|
95
|
-
print(f" Annual O&M: €{opex:,.0f}")
|
|
96
|
-
|
|
97
|
-
job = client.create_planning_job(request)
|
|
98
|
-
print(f"\n Job ID: {job.job_id}")
|
|
99
|
-
print(" Waiting for completion...")
|
|
100
|
-
|
|
101
|
-
result = client.wait_for_completion(job.job_id, poll_interval=30, timeout=7200)
|
|
102
|
-
|
|
103
|
-
print(f" ✅ Completed in {result.summary.solve_time_seconds:.0f}s")
|
|
104
|
-
|
|
105
|
-
if result.summary.investment_metrics:
|
|
106
|
-
metrics = result.summary.investment_metrics
|
|
107
|
-
print(f"\n NPV: €{metrics.npv:>12,.0f}")
|
|
108
|
-
print(f" IRR: {metrics.irr * 100:>12.2f}%")
|
|
109
|
-
print(f" Payback: {metrics.payback_period_years:>12.1f} years")
|
|
110
|
-
|
|
111
|
-
return scenario_name, result
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def main():
|
|
115
|
-
print("=" * 80)
|
|
116
|
-
print("BATTERY CAPACITY SIZING: SCENARIO COMPARISON")
|
|
117
|
-
print("=" * 80)
|
|
118
|
-
print("\nComparing three battery sizes over 10-year horizon")
|
|
119
|
-
print("Goal: Find optimal capacity for maximum NPV")
|
|
120
|
-
|
|
121
|
-
# Initialize client
|
|
122
|
-
client = InvestmentClient(
|
|
123
|
-
base_url="https://api.site-calc.example.com",
|
|
124
|
-
api_key="inv_your_api_key_here",
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# 10-year planning
|
|
128
|
-
timespan = TimeSpan.for_years(2025, 10)
|
|
129
|
-
prices = create_prices(years=10, escalation_rate=0.02)
|
|
130
|
-
|
|
131
|
-
print(f"\nPrices: {len(prices)} hourly values")
|
|
132
|
-
print(f" Year 1 avg: €{sum(prices[:8760]) / 8760:.2f}/MWh")
|
|
133
|
-
print(f" Year 10 avg: €{sum(prices[-8760:]) / 8760:.2f}/MWh")
|
|
134
|
-
|
|
135
|
-
# Test three capacities
|
|
136
|
-
capacities = [10.0, 20.0, 30.0] # MWh
|
|
137
|
-
|
|
138
|
-
scenarios = []
|
|
139
|
-
for capacity in capacities:
|
|
140
|
-
name, result = create_scenario(client, capacity, prices, timespan)
|
|
141
|
-
scenarios.append((name, result))
|
|
142
|
-
|
|
143
|
-
# Compare scenarios
|
|
144
|
-
print("\n" + "=" * 80)
|
|
145
|
-
print("SCENARIO COMPARISON")
|
|
146
|
-
print("=" * 80)
|
|
147
|
-
|
|
148
|
-
names = [s[0] for s in scenarios]
|
|
149
|
-
results = [s[1] for s in scenarios]
|
|
150
|
-
|
|
151
|
-
comparison = compare_scenarios(results, names=names)
|
|
152
|
-
|
|
153
|
-
# Print comparison table
|
|
154
|
-
print(f"\n{'Scenario':<20} {'NPV':>15} {'IRR':>10} {'Payback':>12} {'Revenue':>15} {'Costs':>15}")
|
|
155
|
-
print("-" * 100)
|
|
156
|
-
|
|
157
|
-
for i, name in enumerate(comparison["names"]):
|
|
158
|
-
npv = comparison["npv"][i]
|
|
159
|
-
irr = comparison["irr"][i]
|
|
160
|
-
payback = comparison["payback_years"][i]
|
|
161
|
-
revenue = comparison["total_revenue"][i]
|
|
162
|
-
costs = comparison["total_costs"][i]
|
|
163
|
-
|
|
164
|
-
npv_str = f"€{npv:,.0f}" if npv is not None else "N/A"
|
|
165
|
-
irr_str = f"{irr * 100:.2f}%" if irr is not None else "N/A"
|
|
166
|
-
payback_str = f"{payback:.1f} yrs" if payback is not None else "N/A"
|
|
167
|
-
|
|
168
|
-
print(f"{name:<20} {npv_str:>15} {irr_str:>10} {payback_str:>12} €{revenue:>13,.0f} €{costs:>13,.0f}")
|
|
169
|
-
|
|
170
|
-
# Find optimal
|
|
171
|
-
npv_values = [(i, v) for i, v in enumerate(comparison["npv"]) if v is not None]
|
|
172
|
-
if npv_values:
|
|
173
|
-
best_idx, best_npv = max(npv_values, key=lambda x: x[1])
|
|
174
|
-
best_name = comparison["names"][best_idx]
|
|
175
|
-
|
|
176
|
-
print("\n" + "=" * 80)
|
|
177
|
-
print(f"OPTIMAL CONFIGURATION: {best_name}")
|
|
178
|
-
print("=" * 80)
|
|
179
|
-
print(f" NPV: €{best_npv:,.0f}")
|
|
180
|
-
print(f" IRR: {comparison['irr'][best_idx] * 100:.2f}%")
|
|
181
|
-
print(f" Payback: {comparison['payback_years'][best_idx]:.1f} years")
|
|
182
|
-
print(f" 10y Revenue: €{comparison['total_revenue'][best_idx]:,.0f}")
|
|
183
|
-
print(f" 10y Costs: €{comparison['total_costs'][best_idx]:,.0f}")
|
|
184
|
-
print(f" 10y Profit: €{comparison['profit'][best_idx]:,.0f}")
|
|
185
|
-
print("=" * 80)
|
|
186
|
-
|
|
187
|
-
# Calculate NPV per MWh of capacity
|
|
188
|
-
print("\nCAPEX EFFICIENCY:")
|
|
189
|
-
for i, capacity in enumerate(capacities):
|
|
190
|
-
npv = comparison["npv"][i]
|
|
191
|
-
if npv is not None:
|
|
192
|
-
npv_per_mwh = npv / capacity
|
|
193
|
-
print(f" {capacity:.0f} MWh: €{npv_per_mwh:,.0f} NPV per MWh")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if __name__ == "__main__":
|
|
197
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/financial.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/api/__init__.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/api/client.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/exceptions.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/__init__.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/common.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/devices.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/requests.py
RENAMED
|
File without changes
|
{site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/responses.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|