fidgate-sdk 1.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.py[co]
3
+ .DS_Store
4
+ dist/
5
+ .pytest_cache/
6
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FIDgate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,287 @@
1
+ Metadata-Version: 2.5
2
+ Name: fidgate-sdk
3
+ Version: 1.14.0
4
+ Summary: FIDgate SDK — standalone, curated public data-model surface
5
+ Project-URL: Homepage, https://fidgate.com
6
+ Author: FIDgate
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: battery,bess,energy,fidgate,ppa,project-finance,renewables,sdk,solar
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.27
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: pyarrow>=16.0
21
+ Requires-Dist: pydantic<3.0,>=2.5
22
+ Requires-Dist: pyyaml>=6.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.10; extra == 'dev'
25
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: respx>=0.21; extra == 'dev'
28
+ Requires-Dist: ruff==0.15.15; extra == 'dev'
29
+ Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # fidgate-sdk
33
+
34
+ A standalone, stable **public data-model surface** for FIDgate clients: the DTOs,
35
+ inter-BC contracts, configuration schemas, and value types that cross the system
36
+ boundary — and nothing else (no services, ports, adapters, or domain logic).
37
+
38
+ `fidgate-sdk` is **fully independent**: it carries its own standalone copies of the
39
+ data models and has **no dependency on `fidcore`** in either direction. Installing
40
+ the SDK pulls in only `numpy`, `pydantic`, `httpx` (the HTTP client), `pyyaml`
41
+ (config-document validation and serialization) and `pyarrow` (rendering the run's
42
+ Parquet result tables to CSV), never core's logic. The copies are
43
+ **generated from the FIDgate engine's own model sources** and checked for drift on
44
+ every release, so they never diverge by hand.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install fidgate-sdk
50
+ # or
51
+ uv add fidgate-sdk
52
+ ```
53
+
54
+ Requires Python >= 3.10. The only runtime dependencies are `numpy`, `pydantic`,
55
+ `httpx`, `pyyaml` and `pyarrow`.
56
+
57
+ ## Usage
58
+
59
+ ```python
60
+ from fidgate_sdk import AssetConfigSchema, ProjectResult, ProjectSnapshot
61
+ # or browse by category:
62
+ from fidgate_sdk.models import config, contracts, results, types
63
+ ```
64
+
65
+ These schemas are independent copies, **not** the same classes as `fidcore`'s
66
+ (`fidgate_sdk.AssetConfigSchema is fidcore...AssetConfigSchema` is `False`). The
67
+ engine's entry points validate **dict payloads** internally
68
+ (`AssetConfigSchema.model_validate(...)`), so the supported path is to pass plain
69
+ dicts — the SDK schemas are for typing, construction, and validation on the client
70
+ side, and serialise to the dicts the engine accepts.
71
+
72
+ ### Version configs: typed models or raw YAML
73
+
74
+ `create_version` / `update_version` accept a config as a **path / raw bytes**
75
+ (sent verbatim — comments survive), a **wire-form dict**, or a typed
76
+ **`ProjectConfig`** bundle the SDK serialises to YAML itself:
77
+
78
+ ```python
79
+ from fidgate_sdk import AssetConfigSchema, FidgateClient, ProjectConfig, ProjectTimeline
80
+
81
+ config = ProjectConfig(
82
+ timeline=ProjectTimeline(construction_start_year=2024,
83
+ operation_start_year=2025, operation_end_year=2044),
84
+ asset=AssetConfigSchema.model_validate({...}), # models or plain dicts per section
85
+ commercial={...}, # dicts pass through verbatim
86
+ )
87
+ with FidgateClient() as client:
88
+ client.create_version(project_id, config, data_dir="caso_01/")
89
+ ```
90
+
91
+ Every flavor is **schema-validated locally before the request** against
92
+ `fidgate_sdk.config_document.SECTION_SCHEMAS` — the same models the API
93
+ enforces — raising `ConfigValidationError` (no request issued) on failure;
94
+ `external_data` file references are tolerated anywhere, since the engine
95
+ resolves them before validating sections. Pass `validate=False` to skip the
96
+ local check and let the API decide (the escape hatch when an older SDK meets a
97
+ newer server schema).
98
+
99
+ ### Fluent builder
100
+
101
+ `ProjectBuilder` assembles that document step by step — each call validates its
102
+ piece immediately, and the materialized document is **sparse** (unset optionals
103
+ are truly absent, so defaults stay defaults in the stored YAML). You describe the
104
+ project in the vocabulary you built it with; the builder translates that into
105
+ what the API and the engine need, so no example here writes a dotted config path
106
+ by hand. The builder only assembles: `to_project_config()` hands the result to
107
+ the client as a normal `ProjectConfig`, and everything still travels as ONE
108
+ `create_version` call:
109
+
110
+ ```python
111
+ from fidgate_sdk import FidgateClient, ProjectBuilder, constant_price, hourly_da_price_csv
112
+
113
+ builder = (
114
+ ProjectBuilder("planta_b")
115
+ .set_timeline(construction_start_year=2024,
116
+ operation_start_year=2025, operation_end_year=2044)
117
+ .set_grid_connection(max_injection_mw=40.0)
118
+ .add_solar("PV_1", rated_capacity_mw=30.0, profile_path="data/pv_profile.csv",
119
+ capex_eur=18_000_000, opex_eur=300_000)
120
+ .add_merchant_market("m_da", price=hourly_da_price_csv("data/da_prices.csv"))
121
+ .add_ppa("ppa_1", price=constant_price(55.0), percent=70.0, term_years=15)
122
+ .set_financial(discount_rate_project_pct=7.0, discount_rate_equity_pct=9.0)
123
+ )
124
+ with FidgateClient() as client:
125
+ version = client.create_version(project_id, builder.to_project_config(),
126
+ data_dir="caso_01/")
127
+ ```
128
+
129
+ `add_merchant_market` declares the market **and** the plant's merchant exposure
130
+ on it in one call (the pair `add_market` + `add_market_exposure` used to spell) —
131
+ the engine never infers an exposure, so the declaration stays explicit. Call
132
+ order does not matter: a valid program emits the same document however its calls
133
+ are ordered, and a component added before `set_timeline`, or an exposure naming a
134
+ market declared later, resolves at `to_project_config()`.
135
+
136
+ **Sweeps and scenarios in builder vocabulary.** A `project_optimization` batch
137
+ varies a knob by the id of the thing it belongs to and the kwarg it was built
138
+ with — no config path, and a typo fails locally in `to_project_config()`, never
139
+ after upload:
140
+
141
+ ```python
142
+ from fidgate_sdk import ProjectBuilder, debt, per_mw_cost, span
143
+
144
+ builder.add_kpis("npv_eur", "project_irr", "equity_irr", "lcoe_eur_mwh", "dscr_min")
145
+ builder.sweep("BESS_1", unit_duration_h=[2.0, 4.0], capex=[420_000, 700_000]) # one axis, zipped
146
+ builder.sweep_financial(leverage_pct=span(50.0, 70.0, 3)) # a separate axis
147
+ builder.scenario("ppa_65", ppa_1={"price": 65.0}) # one explicit variant
148
+ ```
149
+
150
+ Kwargs in one `sweep` call are linked (zipped); separate calls multiply. The two
151
+ id-less sections have typed helpers, `sweep_financial` and
152
+ `sweep_grid_connection`; `scenario` reaches the same knobs by target. Debt,
153
+ depreciation, contract commitments and maintenance windows are typed factories
154
+ (`debt(...)`, `depreciation(...)`, `baseload_commitment(...)`,
155
+ `maintenance_window(...)`), and `builder.check()` returns a report of what is
156
+ still missing, what defaulted, what a sweep auto-declared and the batch's scenario
157
+ count, without ever raising. `builder.sweepable()` lists the sweepable knobs each
158
+ declared target currently carries.
159
+
160
+ Every section the engine accepts is reachable. A physics-modelled PV plant
161
+ sharing its site with a tolled battery reads:
162
+
163
+ ```python
164
+ from fidgate_sdk import (
165
+ ProjectBuilder, area_sizing, cec_inverter, cec_module, constant_price,
166
+ hourly_da_price_csv, per_mw_cost, pvgis_tmy_meteo, pvlib_generation, sat_mount,
167
+ )
168
+
169
+ builder = (
170
+ ProjectBuilder("pv_bess_tolling")
171
+ .set_timeline(construction_start_year=2026,
172
+ operation_start_year=2027, operation_end_year=2051)
173
+ .set_site(lat=52.52, lon=13.405, tz="Europe/Berlin", area_net_ha=100.0,
174
+ meteo=pvgis_tmy_meteo(lat=52.52, lon=13.405, year=2027))
175
+ .set_grid_connection(max_injection_mw=100.0)
176
+ .add_pv("PV_75", relative_area=0.75,
177
+ module=cec_module("data/equipment/cec_modules.csv", "Trina Solar TSM-600NEG19RC.20",
178
+ length=2.172, width=1.303),
179
+ inverter=cec_inverter("data/equipment/cec_inverters.csv",
180
+ "Huawei Technologies Co - Ltd : SUN2000-100KTL-USH0 [800V]"),
181
+ mount=sat_mount(axis_tilt=0.0, axis_azimuth=0.0, max_angle=60.0, backtrack=True,
182
+ module_nb=2, module_height=1.5, racking_model="open_rack"),
183
+ sizing=area_sizing(modules_per_string=25, strings_per_inverter=8, gcr=0.6),
184
+ generation=pvlib_generation(temperature_model="pvsyst",
185
+ temperature_model_parameters={"u_c": 29.0, "u_v": 0.0,
186
+ "module_efficiency": 0.21,
187
+ "alpha_absorption": 0.9},
188
+ losses_model="no_loss", inv_to_poi_loss_pct=2.0),
189
+ capex=per_mw_cost(600_000), opex=per_mw_cost(12_000), annual_degradation_pct=0.4)
190
+ .add_battery("BESS_1", unit_rated_power_mw=2.5, unit_duration_h=2.0, nb_units=10,
191
+ capex=per_mw_cost(400_000), opex=per_mw_cost(10_000), lifetime_years=15)
192
+ .set_actors(["owner", "toller"], plant_owner="owner")
193
+ # one call for the market + the toller's exposure; a second exposure adds on
194
+ .add_merchant_market("m_da", price=hourly_da_price_csv("data/da_prices.csv"),
195
+ exposure_id="exp_toller", seller="toller",
196
+ effective_year=2027, term_years=10)
197
+ .add_market_exposure("exp_owner", market_id="m_da", seller="owner")
198
+ .add_tolling("c_tolling_bess", price=constant_price(60_000.0), ceded_components=["BESS_1"],
199
+ seller="owner", buyer="toller", term_years=10)
200
+ .add_ppa("c_ppa_solar", price=constant_price(40.0), quantity_reference="PRODUCTION",
201
+ of=["PV_75"], seller="owner", buyer="toller", term_years=10)
202
+ .set_dispatch(seed=7)
203
+ .set_financial(discount_rate_project_pct=7.0, discount_rate_equity_pct=9.0,
204
+ corporate_tax_rate_pct=20.0)
205
+ )
206
+ ```
207
+
208
+ The helper families, all exported at the top level:
209
+
210
+ | Family | Helpers |
211
+ |---|---|
212
+ | price curves | `constant_price`, `escalating_price`, `hourly_da_price_csv` (`by_year=`), `monthly_profile_price`, `annual_price_csv` |
213
+ | cost models | `absolute_cost`, `per_mw_cost`, `composite_cost` → a side-agnostic `CostModel` for `capex=` / `opex=` |
214
+ | site meteo | `pvgis_tmy_meteo`, `weather_csv_meteo`, `pvgis_tmy` (site-relative) → `set_site(meteo=)` |
215
+ | PV hardware / layout | `cec_module`, `module_parameters`, `cec_inverter`, `fixed_mount`, `sat_mount`, `area_sizing`, `topology_sizing`, `pvlib_generation` |
216
+ | ageing / availability | `generation_degradation`, `linear_degradation`, `soh_curves_degradation`, `global_availability`, `maintenance_windows`, `maintenance_window` |
217
+ | financial pieces | `debt`, `depreciation` → typed factories for `set_financial(debt=…, depreciation=…)` |
218
+ | contract commitments / axes | `baseload_commitment`, `as_produced_commitment`, `quantity_reference`, `quantity_arc`, `quantity_expression`, `per_mwh_shortfall_penalty`, `buyback_penalty` |
219
+ | value lists for a sweep | `span`, `steps` |
220
+
221
+ Contracts have one method per type — `add_ppa`, `add_baseload`,
222
+ `add_baseload_buyback`, `add_tolling`, `add_custom_contract` — so the rules the
223
+ lenient input schema leaves to the engine (a tolling fee accrues per period,
224
+ never hourly; a custom contract names its quantity; a financial settlement names
225
+ a declared reference market) fail at the call that broke them. A buyback names
226
+ its basis explicitly — `add_baseload_buyback(..., buyback_basis=)` /
227
+ `buyback_penalty(market_id, basis=)` with `"fixed_cost"` (buy − strike, signed)
228
+ or `"replacement_cost"` (max(0, buy − strike)); there is no default. Markets take
229
+ `market_type="DA" | "ID" | "imbalance"`; `set_dispatch(...)` writes the optional
230
+ `dispatch:` section; `set_financial(loss_treatment=...)` qualifies the tax block.
231
+ Every helper returns a plain dict or a small frozen dataclass, and every slot
232
+ that takes one also takes the raw `{model_name, model_parameters}` node.
233
+
234
+ ### Assisted wizard
235
+
236
+ Don't know the builder API yet? `ProjectBuilder.assisted()` asks the questions
237
+ instead — main setup first (solar merchant, solar + PPA, solar + battery + PPA,
238
+ custom, or a physics-modelled PV plant with a battery under a tolling agreement),
239
+ then only what that setup needs, with optional extras as skippable follow-ups
240
+ (Enter omits the key, so engine defaults apply at run time). The custom setup
241
+ additionally offers a physics PV component (with the site's weather source), a
242
+ wind component, tolling and baseload contracts, an actor registry and the
243
+ dispatch knobs, each behind a yes/no gate. Its optimization step builds a menu of
244
+ the plant's sweepable knobs (from `ProjectBuilder.sweepable()`) and asks explicit
245
+ scenarios by target and knob — never a dotted config path:
246
+
247
+ ```python
248
+ from fidgate_sdk import FidgateClient, ProjectBuilder
249
+
250
+ config = ProjectBuilder.assisted() # interactive Q&A in the terminal
251
+ with FidgateClient() as client:
252
+ version = client.create_version(project_id, config, data_dir="caso_01/")
253
+ ```
254
+
255
+ The wizard always returns the validated `ProjectConfig`; choosing the
256
+ "YAML file" delivery additionally writes it to a path of your choice. IO is
257
+ injectable (`input_fn` / `output_fn`), so the whole flow can be scripted —
258
+ see `fidgate_sdk.wizard.run_wizard`.
259
+
260
+ `ParameterSweep` is a different thing from `ProjectBuilder.sweep`: it is a **client-side loop** that stores and runs one version per value
261
+ over the built document — one version, one run each, headline KPIs (`feasible`,
262
+ `irr`, `npv_eur`, `energy_to_grid_mwh`) extracted into plain dict rows — whereas
263
+ `builder.sweep(...)` declares ONE server-side `project_optimization` batch. As a
264
+ raw client-side loop it addresses its parameter by config path:
265
+
266
+ ```python
267
+ from fidgate_sdk import ParameterSweep
268
+
269
+ sweep = ParameterSweep(
270
+ parameter="commercial.contracts.ppa_1.pricing.model_parameters.price",
271
+ values=[45.0, 55.0, 65.0],
272
+ )
273
+ rows = sweep.run(client, project_id, builder, into="sweep_results", data_dir="caso_01/")
274
+ ```
275
+
276
+ ### Reading responses: JSON or text
277
+
278
+ Every object the client returns is a typed model (`version.id` keeps working)
279
+ that prints as its JSON body; ask for `.to_text()` when you want something
280
+ readable. Listings are ordinary lists with the same views:
281
+
282
+ ```python
283
+ version = client.get_version(project.id, version.id)
284
+ print(version) # indented JSON; version.to_dict() / .to_json() too
285
+ print(version.to_text()) # one summary line + indented file table
286
+ print(client.list_projects().to_text()) # NAME CREATED UPDATED ID table
287
+ ```
@@ -0,0 +1,256 @@
1
+ # fidgate-sdk
2
+
3
+ A standalone, stable **public data-model surface** for FIDgate clients: the DTOs,
4
+ inter-BC contracts, configuration schemas, and value types that cross the system
5
+ boundary — and nothing else (no services, ports, adapters, or domain logic).
6
+
7
+ `fidgate-sdk` is **fully independent**: it carries its own standalone copies of the
8
+ data models and has **no dependency on `fidcore`** in either direction. Installing
9
+ the SDK pulls in only `numpy`, `pydantic`, `httpx` (the HTTP client), `pyyaml`
10
+ (config-document validation and serialization) and `pyarrow` (rendering the run's
11
+ Parquet result tables to CSV), never core's logic. The copies are
12
+ **generated from the FIDgate engine's own model sources** and checked for drift on
13
+ every release, so they never diverge by hand.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install fidgate-sdk
19
+ # or
20
+ uv add fidgate-sdk
21
+ ```
22
+
23
+ Requires Python >= 3.10. The only runtime dependencies are `numpy`, `pydantic`,
24
+ `httpx`, `pyyaml` and `pyarrow`.
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ from fidgate_sdk import AssetConfigSchema, ProjectResult, ProjectSnapshot
30
+ # or browse by category:
31
+ from fidgate_sdk.models import config, contracts, results, types
32
+ ```
33
+
34
+ These schemas are independent copies, **not** the same classes as `fidcore`'s
35
+ (`fidgate_sdk.AssetConfigSchema is fidcore...AssetConfigSchema` is `False`). The
36
+ engine's entry points validate **dict payloads** internally
37
+ (`AssetConfigSchema.model_validate(...)`), so the supported path is to pass plain
38
+ dicts — the SDK schemas are for typing, construction, and validation on the client
39
+ side, and serialise to the dicts the engine accepts.
40
+
41
+ ### Version configs: typed models or raw YAML
42
+
43
+ `create_version` / `update_version` accept a config as a **path / raw bytes**
44
+ (sent verbatim — comments survive), a **wire-form dict**, or a typed
45
+ **`ProjectConfig`** bundle the SDK serialises to YAML itself:
46
+
47
+ ```python
48
+ from fidgate_sdk import AssetConfigSchema, FidgateClient, ProjectConfig, ProjectTimeline
49
+
50
+ config = ProjectConfig(
51
+ timeline=ProjectTimeline(construction_start_year=2024,
52
+ operation_start_year=2025, operation_end_year=2044),
53
+ asset=AssetConfigSchema.model_validate({...}), # models or plain dicts per section
54
+ commercial={...}, # dicts pass through verbatim
55
+ )
56
+ with FidgateClient() as client:
57
+ client.create_version(project_id, config, data_dir="caso_01/")
58
+ ```
59
+
60
+ Every flavor is **schema-validated locally before the request** against
61
+ `fidgate_sdk.config_document.SECTION_SCHEMAS` — the same models the API
62
+ enforces — raising `ConfigValidationError` (no request issued) on failure;
63
+ `external_data` file references are tolerated anywhere, since the engine
64
+ resolves them before validating sections. Pass `validate=False` to skip the
65
+ local check and let the API decide (the escape hatch when an older SDK meets a
66
+ newer server schema).
67
+
68
+ ### Fluent builder
69
+
70
+ `ProjectBuilder` assembles that document step by step — each call validates its
71
+ piece immediately, and the materialized document is **sparse** (unset optionals
72
+ are truly absent, so defaults stay defaults in the stored YAML). You describe the
73
+ project in the vocabulary you built it with; the builder translates that into
74
+ what the API and the engine need, so no example here writes a dotted config path
75
+ by hand. The builder only assembles: `to_project_config()` hands the result to
76
+ the client as a normal `ProjectConfig`, and everything still travels as ONE
77
+ `create_version` call:
78
+
79
+ ```python
80
+ from fidgate_sdk import FidgateClient, ProjectBuilder, constant_price, hourly_da_price_csv
81
+
82
+ builder = (
83
+ ProjectBuilder("planta_b")
84
+ .set_timeline(construction_start_year=2024,
85
+ operation_start_year=2025, operation_end_year=2044)
86
+ .set_grid_connection(max_injection_mw=40.0)
87
+ .add_solar("PV_1", rated_capacity_mw=30.0, profile_path="data/pv_profile.csv",
88
+ capex_eur=18_000_000, opex_eur=300_000)
89
+ .add_merchant_market("m_da", price=hourly_da_price_csv("data/da_prices.csv"))
90
+ .add_ppa("ppa_1", price=constant_price(55.0), percent=70.0, term_years=15)
91
+ .set_financial(discount_rate_project_pct=7.0, discount_rate_equity_pct=9.0)
92
+ )
93
+ with FidgateClient() as client:
94
+ version = client.create_version(project_id, builder.to_project_config(),
95
+ data_dir="caso_01/")
96
+ ```
97
+
98
+ `add_merchant_market` declares the market **and** the plant's merchant exposure
99
+ on it in one call (the pair `add_market` + `add_market_exposure` used to spell) —
100
+ the engine never infers an exposure, so the declaration stays explicit. Call
101
+ order does not matter: a valid program emits the same document however its calls
102
+ are ordered, and a component added before `set_timeline`, or an exposure naming a
103
+ market declared later, resolves at `to_project_config()`.
104
+
105
+ **Sweeps and scenarios in builder vocabulary.** A `project_optimization` batch
106
+ varies a knob by the id of the thing it belongs to and the kwarg it was built
107
+ with — no config path, and a typo fails locally in `to_project_config()`, never
108
+ after upload:
109
+
110
+ ```python
111
+ from fidgate_sdk import ProjectBuilder, debt, per_mw_cost, span
112
+
113
+ builder.add_kpis("npv_eur", "project_irr", "equity_irr", "lcoe_eur_mwh", "dscr_min")
114
+ builder.sweep("BESS_1", unit_duration_h=[2.0, 4.0], capex=[420_000, 700_000]) # one axis, zipped
115
+ builder.sweep_financial(leverage_pct=span(50.0, 70.0, 3)) # a separate axis
116
+ builder.scenario("ppa_65", ppa_1={"price": 65.0}) # one explicit variant
117
+ ```
118
+
119
+ Kwargs in one `sweep` call are linked (zipped); separate calls multiply. The two
120
+ id-less sections have typed helpers, `sweep_financial` and
121
+ `sweep_grid_connection`; `scenario` reaches the same knobs by target. Debt,
122
+ depreciation, contract commitments and maintenance windows are typed factories
123
+ (`debt(...)`, `depreciation(...)`, `baseload_commitment(...)`,
124
+ `maintenance_window(...)`), and `builder.check()` returns a report of what is
125
+ still missing, what defaulted, what a sweep auto-declared and the batch's scenario
126
+ count, without ever raising. `builder.sweepable()` lists the sweepable knobs each
127
+ declared target currently carries.
128
+
129
+ Every section the engine accepts is reachable. A physics-modelled PV plant
130
+ sharing its site with a tolled battery reads:
131
+
132
+ ```python
133
+ from fidgate_sdk import (
134
+ ProjectBuilder, area_sizing, cec_inverter, cec_module, constant_price,
135
+ hourly_da_price_csv, per_mw_cost, pvgis_tmy_meteo, pvlib_generation, sat_mount,
136
+ )
137
+
138
+ builder = (
139
+ ProjectBuilder("pv_bess_tolling")
140
+ .set_timeline(construction_start_year=2026,
141
+ operation_start_year=2027, operation_end_year=2051)
142
+ .set_site(lat=52.52, lon=13.405, tz="Europe/Berlin", area_net_ha=100.0,
143
+ meteo=pvgis_tmy_meteo(lat=52.52, lon=13.405, year=2027))
144
+ .set_grid_connection(max_injection_mw=100.0)
145
+ .add_pv("PV_75", relative_area=0.75,
146
+ module=cec_module("data/equipment/cec_modules.csv", "Trina Solar TSM-600NEG19RC.20",
147
+ length=2.172, width=1.303),
148
+ inverter=cec_inverter("data/equipment/cec_inverters.csv",
149
+ "Huawei Technologies Co - Ltd : SUN2000-100KTL-USH0 [800V]"),
150
+ mount=sat_mount(axis_tilt=0.0, axis_azimuth=0.0, max_angle=60.0, backtrack=True,
151
+ module_nb=2, module_height=1.5, racking_model="open_rack"),
152
+ sizing=area_sizing(modules_per_string=25, strings_per_inverter=8, gcr=0.6),
153
+ generation=pvlib_generation(temperature_model="pvsyst",
154
+ temperature_model_parameters={"u_c": 29.0, "u_v": 0.0,
155
+ "module_efficiency": 0.21,
156
+ "alpha_absorption": 0.9},
157
+ losses_model="no_loss", inv_to_poi_loss_pct=2.0),
158
+ capex=per_mw_cost(600_000), opex=per_mw_cost(12_000), annual_degradation_pct=0.4)
159
+ .add_battery("BESS_1", unit_rated_power_mw=2.5, unit_duration_h=2.0, nb_units=10,
160
+ capex=per_mw_cost(400_000), opex=per_mw_cost(10_000), lifetime_years=15)
161
+ .set_actors(["owner", "toller"], plant_owner="owner")
162
+ # one call for the market + the toller's exposure; a second exposure adds on
163
+ .add_merchant_market("m_da", price=hourly_da_price_csv("data/da_prices.csv"),
164
+ exposure_id="exp_toller", seller="toller",
165
+ effective_year=2027, term_years=10)
166
+ .add_market_exposure("exp_owner", market_id="m_da", seller="owner")
167
+ .add_tolling("c_tolling_bess", price=constant_price(60_000.0), ceded_components=["BESS_1"],
168
+ seller="owner", buyer="toller", term_years=10)
169
+ .add_ppa("c_ppa_solar", price=constant_price(40.0), quantity_reference="PRODUCTION",
170
+ of=["PV_75"], seller="owner", buyer="toller", term_years=10)
171
+ .set_dispatch(seed=7)
172
+ .set_financial(discount_rate_project_pct=7.0, discount_rate_equity_pct=9.0,
173
+ corporate_tax_rate_pct=20.0)
174
+ )
175
+ ```
176
+
177
+ The helper families, all exported at the top level:
178
+
179
+ | Family | Helpers |
180
+ |---|---|
181
+ | price curves | `constant_price`, `escalating_price`, `hourly_da_price_csv` (`by_year=`), `monthly_profile_price`, `annual_price_csv` |
182
+ | cost models | `absolute_cost`, `per_mw_cost`, `composite_cost` → a side-agnostic `CostModel` for `capex=` / `opex=` |
183
+ | site meteo | `pvgis_tmy_meteo`, `weather_csv_meteo`, `pvgis_tmy` (site-relative) → `set_site(meteo=)` |
184
+ | PV hardware / layout | `cec_module`, `module_parameters`, `cec_inverter`, `fixed_mount`, `sat_mount`, `area_sizing`, `topology_sizing`, `pvlib_generation` |
185
+ | ageing / availability | `generation_degradation`, `linear_degradation`, `soh_curves_degradation`, `global_availability`, `maintenance_windows`, `maintenance_window` |
186
+ | financial pieces | `debt`, `depreciation` → typed factories for `set_financial(debt=…, depreciation=…)` |
187
+ | contract commitments / axes | `baseload_commitment`, `as_produced_commitment`, `quantity_reference`, `quantity_arc`, `quantity_expression`, `per_mwh_shortfall_penalty`, `buyback_penalty` |
188
+ | value lists for a sweep | `span`, `steps` |
189
+
190
+ Contracts have one method per type — `add_ppa`, `add_baseload`,
191
+ `add_baseload_buyback`, `add_tolling`, `add_custom_contract` — so the rules the
192
+ lenient input schema leaves to the engine (a tolling fee accrues per period,
193
+ never hourly; a custom contract names its quantity; a financial settlement names
194
+ a declared reference market) fail at the call that broke them. A buyback names
195
+ its basis explicitly — `add_baseload_buyback(..., buyback_basis=)` /
196
+ `buyback_penalty(market_id, basis=)` with `"fixed_cost"` (buy − strike, signed)
197
+ or `"replacement_cost"` (max(0, buy − strike)); there is no default. Markets take
198
+ `market_type="DA" | "ID" | "imbalance"`; `set_dispatch(...)` writes the optional
199
+ `dispatch:` section; `set_financial(loss_treatment=...)` qualifies the tax block.
200
+ Every helper returns a plain dict or a small frozen dataclass, and every slot
201
+ that takes one also takes the raw `{model_name, model_parameters}` node.
202
+
203
+ ### Assisted wizard
204
+
205
+ Don't know the builder API yet? `ProjectBuilder.assisted()` asks the questions
206
+ instead — main setup first (solar merchant, solar + PPA, solar + battery + PPA,
207
+ custom, or a physics-modelled PV plant with a battery under a tolling agreement),
208
+ then only what that setup needs, with optional extras as skippable follow-ups
209
+ (Enter omits the key, so engine defaults apply at run time). The custom setup
210
+ additionally offers a physics PV component (with the site's weather source), a
211
+ wind component, tolling and baseload contracts, an actor registry and the
212
+ dispatch knobs, each behind a yes/no gate. Its optimization step builds a menu of
213
+ the plant's sweepable knobs (from `ProjectBuilder.sweepable()`) and asks explicit
214
+ scenarios by target and knob — never a dotted config path:
215
+
216
+ ```python
217
+ from fidgate_sdk import FidgateClient, ProjectBuilder
218
+
219
+ config = ProjectBuilder.assisted() # interactive Q&A in the terminal
220
+ with FidgateClient() as client:
221
+ version = client.create_version(project_id, config, data_dir="caso_01/")
222
+ ```
223
+
224
+ The wizard always returns the validated `ProjectConfig`; choosing the
225
+ "YAML file" delivery additionally writes it to a path of your choice. IO is
226
+ injectable (`input_fn` / `output_fn`), so the whole flow can be scripted —
227
+ see `fidgate_sdk.wizard.run_wizard`.
228
+
229
+ `ParameterSweep` is a different thing from `ProjectBuilder.sweep`: it is a **client-side loop** that stores and runs one version per value
230
+ over the built document — one version, one run each, headline KPIs (`feasible`,
231
+ `irr`, `npv_eur`, `energy_to_grid_mwh`) extracted into plain dict rows — whereas
232
+ `builder.sweep(...)` declares ONE server-side `project_optimization` batch. As a
233
+ raw client-side loop it addresses its parameter by config path:
234
+
235
+ ```python
236
+ from fidgate_sdk import ParameterSweep
237
+
238
+ sweep = ParameterSweep(
239
+ parameter="commercial.contracts.ppa_1.pricing.model_parameters.price",
240
+ values=[45.0, 55.0, 65.0],
241
+ )
242
+ rows = sweep.run(client, project_id, builder, into="sweep_results", data_dir="caso_01/")
243
+ ```
244
+
245
+ ### Reading responses: JSON or text
246
+
247
+ Every object the client returns is a typed model (`version.id` keeps working)
248
+ that prints as its JSON body; ask for `.to_text()` when you want something
249
+ readable. Listings are ordinary lists with the same views:
250
+
251
+ ```python
252
+ version = client.get_version(project.id, version.id)
253
+ print(version) # indented JSON; version.to_dict() / .to_json() too
254
+ print(version.to_text()) # one summary line + indented file table
255
+ print(client.list_projects().to_text()) # NAME CREATED UPDATED ID table
256
+ ```