site-calc-investment 1.2.0__tar.gz → 1.2.2__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 (55) hide show
  1. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/CHANGELOG.md +29 -0
  2. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/PKG-INFO +28 -17
  3. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/QUICK_START.md +1 -1
  4. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/README.md +24 -16
  5. site_calc_investment-1.2.2/examples/01_basic_capacity_planning.py +174 -0
  6. site_calc_investment-1.2.2/examples/02_scenario_comparison.py +211 -0
  7. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/pyproject.toml +6 -1
  8. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/__init__.py +1 -1
  9. site_calc_investment-1.2.2/site_calc_investment/mcp/__init__.py +15 -0
  10. site_calc_investment-1.2.2/site_calc_investment/mcp/config.py +34 -0
  11. site_calc_investment-1.2.2/site_calc_investment/mcp/data_loaders.py +171 -0
  12. site_calc_investment-1.2.2/site_calc_investment/mcp/scenario.py +515 -0
  13. site_calc_investment-1.2.2/site_calc_investment/mcp/server.py +704 -0
  14. site_calc_investment-1.2.2/tests/test_mcp/__init__.py +0 -0
  15. site_calc_investment-1.2.2/tests/test_mcp/conftest.py +114 -0
  16. site_calc_investment-1.2.2/tests/test_mcp/test_data_loaders.py +118 -0
  17. site_calc_investment-1.2.2/tests/test_mcp/test_integration.py +230 -0
  18. site_calc_investment-1.2.2/tests/test_mcp/test_mcp_production.py +184 -0
  19. site_calc_investment-1.2.2/tests/test_mcp/test_scenario.py +437 -0
  20. site_calc_investment-1.2.2/tests/test_mcp/test_tools.py +441 -0
  21. site_calc_investment-1.2.2/uv.lock +2431 -0
  22. site_calc_investment-1.2.0/examples/01_basic_capacity_planning.py +0 -172
  23. site_calc_investment-1.2.0/examples/02_scenario_comparison.py +0 -197
  24. site_calc_investment-1.2.0/uv.lock +0 -1052
  25. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/.github/workflows/ci.yml +0 -0
  26. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/.github/workflows/publish.yml +0 -0
  27. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/.gitignore +0 -0
  28. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/CONTRIBUTING.md +0 -0
  29. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/LICENSE +0 -0
  30. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/MIGRATION_GUIDE.md +0 -0
  31. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/READY_TO_PUBLISH.md +0 -0
  32. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/docs/INVESTMENT_CLIENT_SPEC.md +0 -0
  33. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/examples/03_financial_analysis.py +0 -0
  34. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/publish_to_github.bat +0 -0
  35. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/publish_to_github.sh +0 -0
  36. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/analysis/__init__.py +0 -0
  37. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/analysis/comparison.py +0 -0
  38. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/analysis/financial.py +0 -0
  39. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/api/__init__.py +0 -0
  40. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/api/client.py +0 -0
  41. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/exceptions.py +0 -0
  42. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/models/__init__.py +0 -0
  43. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/models/common.py +0 -0
  44. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/models/devices.py +0 -0
  45. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/models/requests.py +0 -0
  46. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/site_calc_investment/models/responses.py +0 -0
  47. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/conftest.py +0 -0
  48. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/test_api_client.py +0 -0
  49. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/test_common_models.py +0 -0
  50. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/test_device_models.py +0 -0
  51. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/test_financial_analysis.py +0 -0
  52. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/test_production.py +0 -0
  53. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/test_request_models.py +0 -0
  54. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/tests/test_scenario_comparison.py +0 -0
  55. {site_calc_investment-1.2.0 → site_calc_investment-1.2.2}/verify_ready.py +0 -0
@@ -5,6 +5,35 @@ 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.2] - 2026-02-04
9
+
10
+ ### Added
11
+ - **MCP Server**: FastMCP-based MCP server exposing 14 tools for LLM-driven investment planning
12
+ - Stateful builder pattern: create scenario -> add devices -> set timespan -> review -> submit -> get results
13
+ - All 10 device types supported with data shorthand (scalar expansion, CSV/JSON file loading)
14
+ - 3 result detail levels: summary, monthly, full
15
+ - Install via `pip install site-calc-investment[mcp]`
16
+ - CLI entry point: `site-calc-investment-mcp`
17
+ - **`get_device_schema` tool**: Returns property schemas for each device type with types, units, and examples
18
+
19
+ ### Changed
20
+ - Package now has `[mcp]` optional dependency group (`fastmcp>=2.0`)
21
+
22
+ ---
23
+
24
+ ## [1.2.1] - 2026-02-03
25
+
26
+ ### Fixed
27
+ - **README Quick Start**: Fixed example code to use correct model classes and valid parameter values
28
+ - Use `TimeSpanInvestment` instead of `TimeSpan`
29
+ - Use valid `objective` values (`maximize_profit`, `minimize_cost`, `maximize_self_consumption`)
30
+ - Add required `project_lifetime_years` to `InvestmentParameters`
31
+ - Fix `time_limit_seconds` max value (900, not 3600)
32
+ - **Capabilities table**: Corrected timeout from "3600 seconds" to "900 seconds (15 minutes) max"
33
+ - **QUICK_START.md**: Added `pypi` environment name for Trusted Publishing setup
34
+
35
+ ---
36
+
8
37
  ## [1.2.0] - 2026-02-03
9
38
 
10
39
  ### 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.2
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
@@ -25,6 +25,7 @@ Requires-Dist: numpy>=1.24
25
25
  Requires-Dist: pydantic>=2.0
26
26
  Requires-Dist: python-dateutil>=2.8
27
27
  Provides-Extra: dev
28
+ Requires-Dist: fastmcp>=2.0; extra == 'dev'
28
29
  Requires-Dist: mypy>=1.0; extra == 'dev'
29
30
  Requires-Dist: pandas>=2.0; extra == 'dev'
30
31
  Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
@@ -33,6 +34,8 @@ Requires-Dist: pytest-timeout>=2.2; extra == 'dev'
33
34
  Requires-Dist: pytest>=7.0; extra == 'dev'
34
35
  Requires-Dist: ruff>=0.1; extra == 'dev'
35
36
  Requires-Dist: tzdata>=2024.1; extra == 'dev'
37
+ Provides-Extra: mcp
38
+ Requires-Dist: fastmcp>=2.0; extra == 'mcp'
36
39
  Description-Content-Type: text/markdown
37
40
 
38
41
  # Site-Calc Investment Client
@@ -52,9 +55,10 @@ from datetime import datetime
52
55
  from zoneinfo import ZoneInfo
53
56
  from site_calc_investment import InvestmentClient
54
57
  from site_calc_investment.models import (
55
- TimeSpan, Resolution, Site, Battery, ElectricityImport,
58
+ Resolution, Site, Battery, ElectricityImport, ElectricityExport,
56
59
  InvestmentPlanningRequest, InvestmentParameters, OptimizationConfig
57
60
  )
61
+ from site_calc_investment.models.requests import TimeSpanInvestment
58
62
 
59
63
  # Initialize client
60
64
  client = InvestmentClient(
@@ -62,13 +66,16 @@ client = InvestmentClient(
62
66
  api_key="inv_your_api_key_here"
63
67
  )
64
68
 
65
- # Create 10-year planning horizon (1-hour resolution)
66
- timespan = TimeSpan(
69
+ # Create 1-week planning horizon (1-hour resolution)
70
+ timespan = TimeSpanInvestment(
67
71
  start=datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Prague")),
68
- intervals=87600, # 10 years × 8760 hours/year
72
+ intervals=168, # 1 week = 7 days × 24 hours
69
73
  resolution=Resolution.HOUR_1
70
74
  )
71
75
 
76
+ # Generate hourly prices (example: day/night pattern)
77
+ prices = [30.0 if h % 24 < 6 else 80.0 if 8 <= h % 24 < 20 else 50.0 for h in range(168)]
78
+
72
79
  # Define devices (NO ancillary_services field)
73
80
  battery = Battery(
74
81
  name="Battery1",
@@ -82,14 +89,20 @@ battery = Battery(
82
89
 
83
90
  grid_import = ElectricityImport(
84
91
  name="GridImport",
85
- properties={"price": prices_10y, "max_import": 8.0}
92
+ properties={"price": prices, "max_import": 10.0}
93
+ )
94
+
95
+ grid_export = ElectricityExport(
96
+ name="GridExport",
97
+ properties={"price": prices, "max_export": 10.0}
86
98
  )
87
99
 
88
- site = Site(site_id="investment_site", devices=[battery, grid_import])
100
+ site = Site(site_id="investment_site", devices=[battery, grid_import, grid_export])
89
101
 
90
102
  # Investment parameters
91
103
  inv_params = InvestmentParameters(
92
104
  discount_rate=0.05,
105
+ project_lifetime_years=10, # Required field
93
106
  device_capital_costs={"Battery1": 500000}, # €500k CAPEX
94
107
  device_annual_opex={"Battery1": 5000} # €5k/year O&M
95
108
  )
@@ -100,19 +113,17 @@ request = InvestmentPlanningRequest(
100
113
  timespan=timespan,
101
114
  investment_parameters=inv_params,
102
115
  optimization_config=OptimizationConfig(
103
- objective="maximize_npv",
104
- time_limit_seconds=3600
116
+ objective="maximize_profit", # Options: maximize_profit, minimize_cost, maximize_self_consumption
117
+ time_limit_seconds=300 # Max 900 seconds (15 min)
105
118
  )
106
119
  )
107
120
 
108
121
  job = client.create_planning_job(request)
109
- result = client.wait_for_completion(job.job_id, poll_interval=30, timeout=7200)
122
+ result = client.wait_for_completion(job.job_id, poll_interval=5, timeout=600)
110
123
 
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")
124
+ print(f"Status: {result.status}")
125
+ print(f"Solver: {result.summary.solver_status}")
126
+ print(f"Profit: €{result.summary.expected_profit:,.2f}")
116
127
  ```
117
128
 
118
129
  ## Features
@@ -135,7 +146,7 @@ print(f"Payback: {metrics.payback_period_years:.1f} years")
135
146
  | Resolution | 1-hour only |
136
147
  | ANS Support | No |
137
148
  | Binary Variables | Relaxed to continuous |
138
- | Timeout | 3600 seconds (1 hour) |
149
+ | Timeout | 900 seconds (15 minutes) max |
139
150
 
140
151
  ## Supported Devices
141
152
 
@@ -203,7 +214,7 @@ print(comparison) # DataFrame with NPV, IRR, costs, revenues
203
214
 
204
215
  ## Documentation
205
216
 
206
- Full documentation available at: https://docs.site-calc.example.com/investment-client
217
+ Full documentation available at: https://github.com/stranma/site-calc-investment#readme
207
218
 
208
219
  ## Examples
209
220
 
@@ -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,174 @@
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
+ """Run basic capacity planning example with 1-week battery optimization."""
26
+ # Get credentials from environment
27
+ api_url = os.environ.get("INVESTMENT_API_URL_DEV") or os.environ.get("INVESTMENT_API_URL")
28
+ api_key = os.environ.get("INVESTMENT_API_KEY_DEV") or os.environ.get("INVESTMENT_API_KEY")
29
+
30
+ if not api_url or not api_key:
31
+ print("ERROR: Set INVESTMENT_API_URL and INVESTMENT_API_KEY environment variables")
32
+ return
33
+
34
+ # Initialize client
35
+ client = InvestmentClient(
36
+ base_url=api_url,
37
+ api_key=api_key,
38
+ )
39
+
40
+ # Create 1-week planning horizon (1-hour resolution)
41
+ timespan = TimeSpanInvestment(
42
+ start=datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Prague")),
43
+ intervals=168, # 1 week = 7 days x 24 hours
44
+ )
45
+ print(f"Planning horizon: {timespan.intervals} hourly intervals ({timespan.intervals / 24:.0f} days)")
46
+
47
+ # Generate price profile with day/night pattern
48
+ prices = []
49
+ for hour in range(168):
50
+ hour_of_day = hour % 24
51
+ if 9 <= hour_of_day <= 20:
52
+ prices.append(80.0) # Day: high price
53
+ else:
54
+ prices.append(30.0) # Night: low price
55
+
56
+ print(f"Price profile generated: {len(prices)} values")
57
+ print(" Day price: EUR 80/MWh, Night price: EUR 30/MWh")
58
+
59
+ # Define 10 MW / 20 MWh battery (2-hour duration)
60
+ battery = Battery(
61
+ name="Battery1",
62
+ properties={
63
+ "capacity": 20.0, # MWh
64
+ "max_power": 10.0, # MW (2-hour discharge)
65
+ "efficiency": 0.90, # 90% round-trip
66
+ "initial_soc": 0.5, # Start at 50%
67
+ },
68
+ )
69
+
70
+ # Market devices (grid connections)
71
+ grid_import = ElectricityImport(
72
+ name="GridImport",
73
+ properties={"price": prices, "max_import": 20.0},
74
+ )
75
+
76
+ grid_export = ElectricityExport(
77
+ name="GridExport",
78
+ properties={"price": prices, "max_export": 20.0},
79
+ )
80
+
81
+ # Create site
82
+ site = Site(
83
+ site_id="battery_investment_site",
84
+ description="Battery capacity planning example",
85
+ devices=[battery, grid_import, grid_export],
86
+ )
87
+
88
+ # Investment parameters
89
+ inv_params = InvestmentParameters(
90
+ discount_rate=0.05, # 5% discount rate
91
+ project_lifetime_years=10, # Required field
92
+ device_capital_costs={
93
+ "Battery1": 2_000_000 # EUR 2M CAPEX (EUR 100/kWh)
94
+ },
95
+ device_annual_opex={
96
+ "Battery1": 20_000 # EUR 20k/year O&M
97
+ },
98
+ )
99
+
100
+ # Create optimization request
101
+ request = InvestmentPlanningRequest(
102
+ sites=[site],
103
+ timespan=timespan,
104
+ investment_parameters=inv_params,
105
+ optimization_config=OptimizationConfig(
106
+ objective="maximize_profit", # Options: maximize_profit, minimize_cost, maximize_self_consumption
107
+ time_limit_seconds=300, # 5 minutes max
108
+ relax_binary_variables=True,
109
+ ),
110
+ )
111
+
112
+ print("\n" + "=" * 60)
113
+ print("Submitting optimization job...")
114
+ print("=" * 60)
115
+
116
+ # Submit job
117
+ job = client.create_planning_job(request)
118
+ print(f"\nJob ID: {job.job_id}")
119
+ print(f"Status: {job.status}")
120
+
121
+ # Wait for completion
122
+ print("\nWaiting for optimization to complete...")
123
+
124
+ result = client.wait_for_completion(
125
+ job.job_id,
126
+ poll_interval=5, # Check every 5 seconds
127
+ timeout=600, # 10 minute maximum
128
+ )
129
+
130
+ print("\n" + "=" * 60)
131
+ print("OPTIMIZATION RESULTS")
132
+ print("=" * 60)
133
+
134
+ # Summary
135
+ summary = result.summary
136
+ print(f"\nSolver Status: {summary.solver_status}")
137
+ print(f"Solve Time: {summary.solve_time_seconds:.1f}s")
138
+
139
+ if summary.expected_profit is not None:
140
+ print(f"Expected Profit: EUR {summary.expected_profit:,.2f}")
141
+
142
+ # Investment metrics (if available)
143
+ if result.investment_metrics:
144
+ metrics = result.investment_metrics
145
+ print("\nINVESTMENT METRICS:")
146
+ if metrics.total_revenue_10y is not None:
147
+ print(f" Total Revenue (10y): EUR {metrics.total_revenue_10y:>15,.0f}")
148
+ if metrics.total_costs_10y is not None:
149
+ print(f" Total Costs (10y): EUR {metrics.total_costs_10y:>15,.0f}")
150
+ if metrics.npv is not None:
151
+ print(f" NPV: EUR {metrics.npv:>15,.0f}")
152
+ if metrics.irr is not None:
153
+ print(f" IRR: {metrics.irr * 100:>15.2f}%")
154
+ if metrics.payback_period_years is not None:
155
+ print(f" Payback Period: {metrics.payback_period_years:>15.1f} years")
156
+
157
+ # Device schedules (first 24 hours)
158
+ site_result = result.sites.get("battery_investment_site")
159
+ if site_result:
160
+ battery_schedule = site_result.device_schedules.get("Battery1")
161
+ if battery_schedule and battery_schedule.flows.get("electricity"):
162
+ print("\nBATTERY OPERATION (First 24 hours):")
163
+ el_flow = battery_schedule.flows["electricity"]
164
+ soc = battery_schedule.soc or []
165
+
166
+ for hour in range(min(24, len(el_flow))):
167
+ soc_str = f"{soc[hour]:.1%}" if hour < len(soc) else "N/A"
168
+ print(f" Hour {hour:2d}: Power {el_flow[hour]:>7.2f} MW | SOC {soc_str}")
169
+
170
+ print("\n" + "=" * 60)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
@@ -0,0 +1,211 @@
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
+ """Run scenario comparison example comparing three battery sizes."""
129
+ print("=" * 60)
130
+ print("BATTERY CAPACITY SIZING: SCENARIO COMPARISON")
131
+ print("=" * 60)
132
+ print("\nComparing three battery sizes over 1-week horizon")
133
+ print("Goal: Find optimal capacity for maximum profit")
134
+
135
+ # Get credentials from environment
136
+ api_url = os.environ.get("INVESTMENT_API_URL_DEV") or os.environ.get("INVESTMENT_API_URL")
137
+ api_key = os.environ.get("INVESTMENT_API_KEY_DEV") or os.environ.get("INVESTMENT_API_KEY")
138
+
139
+ if not api_url or not api_key:
140
+ print("\nERROR: Set INVESTMENT_API_URL and INVESTMENT_API_KEY environment variables")
141
+ return
142
+
143
+ # Initialize client
144
+ client = InvestmentClient(
145
+ base_url=api_url,
146
+ api_key=api_key,
147
+ )
148
+
149
+ # 1-week planning
150
+ timespan = TimeSpanInvestment(
151
+ start=datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Prague")),
152
+ intervals=168, # 1 week
153
+ )
154
+ prices = create_prices(days=7)
155
+
156
+ print(f"\nPrices: {len(prices)} hourly values")
157
+ print(" Day price: EUR 80/MWh, Night price: EUR 30/MWh")
158
+
159
+ # Test three capacities
160
+ capacities = [10.0, 20.0, 30.0] # MWh
161
+
162
+ scenarios = []
163
+ for capacity in capacities:
164
+ name, result = create_scenario(client, capacity, prices, timespan)
165
+ scenarios.append((name, result))
166
+
167
+ # Compare scenarios
168
+ print("\n" + "=" * 60)
169
+ print("SCENARIO COMPARISON")
170
+ print("=" * 60)
171
+
172
+ names = [s[0] for s in scenarios]
173
+ results = [s[1] for s in scenarios]
174
+
175
+ comparison = compare_scenarios(results, names=names)
176
+
177
+ # Print comparison table
178
+ print(f"\n{'Scenario':<20} {'Profit':>15} {'NPV':>15} {'IRR':>10}")
179
+ print("-" * 65)
180
+
181
+ for i, name in enumerate(comparison["names"]):
182
+ profit = comparison.get("profit", [None] * len(names))[i]
183
+ npv = comparison.get("npv", [None] * len(names))[i]
184
+ irr = comparison.get("irr", [None] * len(names))[i]
185
+
186
+ profit_str = f"EUR {profit:,.0f}" if profit is not None else "N/A"
187
+ npv_str = f"EUR {npv:,.0f}" if npv is not None else "N/A"
188
+ irr_str = f"{irr * 100:.2f}%" if irr is not None else "N/A"
189
+
190
+ print(f"{name:<20} {profit_str:>15} {npv_str:>15} {irr_str:>10}")
191
+
192
+ # Find optimal by profit
193
+ profit_values = comparison.get("profit", [])
194
+ if profit_values:
195
+ valid_profits = [(i, v) for i, v in enumerate(profit_values) if v is not None]
196
+ if valid_profits:
197
+ best_idx, best_profit = max(valid_profits, key=lambda x: x[1])
198
+ best_name = comparison["names"][best_idx]
199
+
200
+ print("\n" + "=" * 60)
201
+ print(f"OPTIMAL CONFIGURATION: {best_name}")
202
+ print("=" * 60)
203
+ print(f" Profit: EUR {best_profit:,.0f}")
204
+ else:
205
+ print("\nNo profit data available for comparison")
206
+
207
+ print("=" * 60)
208
+
209
+
210
+ if __name__ == "__main__":
211
+ main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "site-calc-investment"
3
- version = "1.2.0"
3
+ version = "1.2.2"
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"
@@ -29,6 +29,7 @@ dependencies = [
29
29
  ]
30
30
 
31
31
  [project.optional-dependencies]
32
+ mcp = ["fastmcp>=2.0"]
32
33
  dev = [
33
34
  "pytest>=7.0",
34
35
  "pytest-cov>=4.0",
@@ -38,8 +39,12 @@ dev = [
38
39
  "mypy>=1.0",
39
40
  "pandas>=2.0", # For scenario comparison
40
41
  "tzdata>=2024.1", # Timezone data for Windows
42
+ "fastmcp>=2.0", # For MCP server tests
41
43
  ]
42
44
 
45
+ [project.scripts]
46
+ site-calc-investment-mcp = "site_calc_investment.mcp:main"
47
+
43
48
  [project.urls]
44
49
  Homepage = "https://github.com/stranma/site-calc-investment"
45
50
  Documentation = "https://github.com/stranma/site-calc-investment#readme"
@@ -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.2"
7
7
 
8
8
  from site_calc_investment.analysis import (
9
9
  aggregate_annual,