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.
- fidgate_sdk-1.14.0/.gitignore +6 -0
- fidgate_sdk-1.14.0/LICENSE +21 -0
- fidgate_sdk-1.14.0/PKG-INFO +287 -0
- fidgate_sdk-1.14.0/README.md +256 -0
- fidgate_sdk-1.14.0/fidgate_sdk/__init__.py +188 -0
- fidgate_sdk-1.14.0/fidgate_sdk/_knobs.py +741 -0
- fidgate_sdk-1.14.0/fidgate_sdk/_serialize.py +84 -0
- fidgate_sdk-1.14.0/fidgate_sdk/builder.py +4110 -0
- fidgate_sdk-1.14.0/fidgate_sdk/client.py +910 -0
- fidgate_sdk-1.14.0/fidgate_sdk/config_document.py +209 -0
- fidgate_sdk-1.14.0/fidgate_sdk/exceptions.py +256 -0
- fidgate_sdk-1.14.0/fidgate_sdk/models/__init__.py +32 -0
- fidgate_sdk-1.14.0/fidgate_sdk/models/commercial.py +650 -0
- fidgate_sdk-1.14.0/fidgate_sdk/models/config.py +1122 -0
- fidgate_sdk-1.14.0/fidgate_sdk/models/contracts.py +809 -0
- fidgate_sdk-1.14.0/fidgate_sdk/models/optimization.py +325 -0
- fidgate_sdk-1.14.0/fidgate_sdk/models/results.py +86 -0
- fidgate_sdk-1.14.0/fidgate_sdk/models/types.py +716 -0
- fidgate_sdk-1.14.0/fidgate_sdk/overrides.py +381 -0
- fidgate_sdk-1.14.0/fidgate_sdk/presentation.py +124 -0
- fidgate_sdk-1.14.0/fidgate_sdk/projects.py +228 -0
- fidgate_sdk-1.14.0/fidgate_sdk/py.typed +0 -0
- fidgate_sdk-1.14.0/fidgate_sdk/report.py +190 -0
- fidgate_sdk-1.14.0/fidgate_sdk/responses.py +141 -0
- fidgate_sdk-1.14.0/fidgate_sdk/sweep.py +219 -0
- fidgate_sdk-1.14.0/fidgate_sdk/wizard.py +840 -0
- fidgate_sdk-1.14.0/pyproject.toml +65 -0
|
@@ -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
|
+
```
|