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.
Files changed (42) hide show
  1. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/CHANGELOG.md +13 -0
  2. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/PKG-INFO +25 -17
  3. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/QUICK_START.md +1 -1
  4. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/README.md +24 -16
  5. site_calc_investment-1.2.1/examples/01_basic_capacity_planning.py +173 -0
  6. site_calc_investment-1.2.1/examples/02_scenario_comparison.py +210 -0
  7. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/pyproject.toml +1 -1
  8. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/__init__.py +1 -1
  9. site_calc_investment-1.2.0/examples/01_basic_capacity_planning.py +0 -172
  10. site_calc_investment-1.2.0/examples/02_scenario_comparison.py +0 -197
  11. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/.github/workflows/ci.yml +0 -0
  12. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/.github/workflows/publish.yml +0 -0
  13. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/.gitignore +0 -0
  14. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/CONTRIBUTING.md +0 -0
  15. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/LICENSE +0 -0
  16. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/MIGRATION_GUIDE.md +0 -0
  17. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/READY_TO_PUBLISH.md +0 -0
  18. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/docs/INVESTMENT_CLIENT_SPEC.md +0 -0
  19. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/examples/03_financial_analysis.py +0 -0
  20. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/publish_to_github.bat +0 -0
  21. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/publish_to_github.sh +0 -0
  22. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/__init__.py +0 -0
  23. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/comparison.py +0 -0
  24. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/analysis/financial.py +0 -0
  25. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/api/__init__.py +0 -0
  26. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/api/client.py +0 -0
  27. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/exceptions.py +0 -0
  28. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/__init__.py +0 -0
  29. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/common.py +0 -0
  30. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/devices.py +0 -0
  31. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/requests.py +0 -0
  32. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/site_calc_investment/models/responses.py +0 -0
  33. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/conftest.py +0 -0
  34. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_api_client.py +0 -0
  35. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_common_models.py +0 -0
  36. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_device_models.py +0 -0
  37. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_financial_analysis.py +0 -0
  38. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_production.py +0 -0
  39. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_request_models.py +0 -0
  40. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/tests/test_scenario_comparison.py +0 -0
  41. {site_calc_investment-1.2.0 → site_calc_investment-1.2.1}/uv.lock +0 -0
  42. {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.0
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
- TimeSpan, Resolution, Site, Battery, ElectricityImport,
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 10-year planning horizon (1-hour resolution)
66
- timespan = 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=87600, # 10 years × 8760 hours/year
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": prices_10y, "max_import": 8.0}
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="maximize_npv",
104
- time_limit_seconds=3600
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=30, timeout=7200)
119
+ result = client.wait_for_completion(job.job_id, poll_interval=5, timeout=600)
110
120
 
111
- # Display investment metrics
112
- metrics = result.summary.investment_metrics
113
- print(f"NPV: €{metrics.npv:,.0f}")
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 | 3600 seconds (1 hour) |
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://docs.site-calc.example.com/investment-client
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: (leave empty)
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
- TimeSpan, Resolution, Site, Battery, ElectricityImport,
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 10-year planning horizon (1-hour resolution)
29
- timespan = 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=87600, # 10 years × 8760 hours/year
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": prices_10y, "max_import": 8.0}
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="maximize_npv",
67
- time_limit_seconds=3600
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=30, timeout=7200)
82
+ result = client.wait_for_completion(job.job_id, poll_interval=5, timeout=600)
73
83
 
74
- # Display investment metrics
75
- metrics = result.summary.investment_metrics
76
- print(f"NPV: €{metrics.npv:,.0f}")
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 | 3600 seconds (1 hour) |
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://docs.site-calc.example.com/investment-client
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "site-calc-investment"
3
- version = "1.2.0"
3
+ version = "1.2.1"
4
4
  description = "Python client for Site-Calc investment planning (capacity sizing, ROI analysis)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  Python client for long-term capacity planning and investment ROI analysis.
4
4
  """
5
5
 
6
- __version__ = "1.2.0"
6
+ __version__ = "1.2.1"
7
7
 
8
8
  from site_calc_investment.analysis import (
9
9
  aggregate_annual,
@@ -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()