validibot-shared 0.1.0__py3-none-any.whl
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.
- validibot_shared/__init__.py +16 -0
- validibot_shared/energyplus/__init__.py +25 -0
- validibot_shared/energyplus/envelopes.py +300 -0
- validibot_shared/energyplus/models.py +140 -0
- validibot_shared/fmi/__init__.py +6 -0
- validibot_shared/fmi/envelopes.py +190 -0
- validibot_shared/fmi/models.py +70 -0
- validibot_shared/py.typed +0 -0
- validibot_shared/validations/__init__.py +0 -0
- validibot_shared/validations/envelopes.py +607 -0
- validibot_shared-0.1.0.dist-info/METADATA +168 -0
- validibot_shared-0.1.0.dist-info/RECORD +15 -0
- validibot_shared-0.1.0.dist-info/WHEEL +4 -0
- validibot_shared-0.1.0.dist-info/licenses/LICENSE +21 -0
- validibot_shared-0.1.0.dist-info/licenses/NOTICE +23 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared domain models and utilities for Simple Validations."""
|
|
2
|
+
|
|
3
|
+
from validibot_shared.energyplus.models import (
|
|
4
|
+
EnergyPlusSimulationLogs,
|
|
5
|
+
EnergyPlusSimulationMetrics,
|
|
6
|
+
EnergyPlusSimulationOutputs,
|
|
7
|
+
)
|
|
8
|
+
from validibot_shared.fmi.models import FMIProbeResult, FMIVariableMeta
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"EnergyPlusSimulationLogs",
|
|
12
|
+
"EnergyPlusSimulationMetrics",
|
|
13
|
+
"EnergyPlusSimulationOutputs",
|
|
14
|
+
"FMIProbeResult",
|
|
15
|
+
"FMIVariableMeta",
|
|
16
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""EnergyPlus integration models and envelopes."""
|
|
2
|
+
|
|
3
|
+
# Reusable output models (components used within envelope classes)
|
|
4
|
+
# Typed envelope subclasses for validator containers
|
|
5
|
+
from .envelopes import (
|
|
6
|
+
EnergyPlusInputEnvelope,
|
|
7
|
+
EnergyPlusInputs,
|
|
8
|
+
EnergyPlusOutputEnvelope,
|
|
9
|
+
EnergyPlusOutputs,
|
|
10
|
+
)
|
|
11
|
+
from .models import (
|
|
12
|
+
EnergyPlusSimulationLogs,
|
|
13
|
+
EnergyPlusSimulationMetrics,
|
|
14
|
+
EnergyPlusSimulationOutputs,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"EnergyPlusInputEnvelope",
|
|
19
|
+
"EnergyPlusInputs",
|
|
20
|
+
"EnergyPlusOutputEnvelope",
|
|
21
|
+
"EnergyPlusOutputs",
|
|
22
|
+
"EnergyPlusSimulationLogs",
|
|
23
|
+
"EnergyPlusSimulationMetrics",
|
|
24
|
+
"EnergyPlusSimulationOutputs",
|
|
25
|
+
]
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EnergyPlus-specific envelope schemas for Advanced validator communication.
|
|
3
|
+
|
|
4
|
+
EnergyPlus is an Advanced validator - it runs in a separate container/service rather
|
|
5
|
+
than within the Django app. These schemas extend the base validation envelopes with
|
|
6
|
+
EnergyPlus-specific typed inputs and outputs.
|
|
7
|
+
|
|
8
|
+
## Architecture Pattern
|
|
9
|
+
|
|
10
|
+
This module demonstrates the envelope subclassing pattern:
|
|
11
|
+
|
|
12
|
+
1. **Base envelopes** (in validibot_shared.validations.envelopes):
|
|
13
|
+
- ValidationInputEnvelope has inputs: dict[str, Any]
|
|
14
|
+
- ValidationOutputEnvelope has outputs: dict[str, Any] | None
|
|
15
|
+
|
|
16
|
+
2. **Domain-specific envelopes** (this file):
|
|
17
|
+
- EnergyPlusInputEnvelope has inputs: EnergyPlusInputs (typed override!)
|
|
18
|
+
- EnergyPlusOutputEnvelope has outputs: EnergyPlusOutputs (typed override!)
|
|
19
|
+
|
|
20
|
+
This provides type safety while maintaining a consistent interface across
|
|
21
|
+
all Advanced validators.
|
|
22
|
+
|
|
23
|
+
## Reusing Component Models
|
|
24
|
+
|
|
25
|
+
The EnergyPlusOutputs class composes existing models from models.py:
|
|
26
|
+
- EnergyPlusSimulationOutputs: File paths (SQL, CSV, ERR, ESO)
|
|
27
|
+
- EnergyPlusSimulationMetrics: Extracted metrics (electricity, gas, EUI)
|
|
28
|
+
- EnergyPlusSimulationLogs: Log tails (stdout, stderr, err file)
|
|
29
|
+
|
|
30
|
+
This follows DRY principles - we don't duplicate data structures. The models.py
|
|
31
|
+
file contains reusable components that represent EnergyPlus simulation outputs.
|
|
32
|
+
These are composed within the envelope classes to provide type-safe output packaging.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from pydantic import BaseModel, Field
|
|
38
|
+
|
|
39
|
+
from validibot_shared.energyplus.models import (
|
|
40
|
+
EnergyPlusSimulationLogs,
|
|
41
|
+
EnergyPlusSimulationMetrics,
|
|
42
|
+
EnergyPlusSimulationOutputs,
|
|
43
|
+
InvocationMode,
|
|
44
|
+
)
|
|
45
|
+
from validibot_shared.validations.envelopes import (
|
|
46
|
+
ValidationInputEnvelope,
|
|
47
|
+
ValidationOutputEnvelope,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# ==============================================================================
|
|
51
|
+
# EnergyPlus Input Configuration
|
|
52
|
+
# ==============================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class EnergyPlusInputs(BaseModel):
|
|
56
|
+
"""
|
|
57
|
+
EnergyPlus simulation configuration parameters.
|
|
58
|
+
|
|
59
|
+
These are the settings that control how the EnergyPlus simulation runs.
|
|
60
|
+
Input files (IDF model, EPW weather) are passed separately in the parent
|
|
61
|
+
envelope's input_files field with roles like 'primary-model' and 'weather'.
|
|
62
|
+
|
|
63
|
+
## Why Separate From Input Files?
|
|
64
|
+
|
|
65
|
+
Configuration parameters (this class) are different from input files:
|
|
66
|
+
- Configuration: How to run the simulation (timesteps, invocation mode, etc.)
|
|
67
|
+
- Input files: What to run (IDF model, weather data)
|
|
68
|
+
|
|
69
|
+
This separation allows:
|
|
70
|
+
- Type-safe configuration validation
|
|
71
|
+
- Reusable models with different run settings
|
|
72
|
+
- Clear distinction between data files and execution parameters
|
|
73
|
+
|
|
74
|
+
## Architecture Design
|
|
75
|
+
|
|
76
|
+
This configuration model is separate from input files to maintain clear
|
|
77
|
+
separation of concerns:
|
|
78
|
+
- Configuration (this class): How to run the simulation
|
|
79
|
+
- Input files (in parent envelope): What files to process
|
|
80
|
+
|
|
81
|
+
Using a typed Pydantic model provides compile-time type checking and
|
|
82
|
+
runtime validation of all configuration parameters.
|
|
83
|
+
|
|
84
|
+
Note: The validator always returns a fixed set of output signals defined
|
|
85
|
+
in its catalog. Users don't need to specify which outputs they want -
|
|
86
|
+
they get all defined signals and write assertions against the ones they
|
|
87
|
+
care about.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
# Simulation timestep configuration
|
|
91
|
+
timestep_per_hour: int = Field(
|
|
92
|
+
default=4,
|
|
93
|
+
description="Number of timesteps per hour (e.g., 4 = 15-minute intervals)",
|
|
94
|
+
ge=1,
|
|
95
|
+
le=60,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Optional run period override
|
|
99
|
+
run_period_days: int | None = Field(
|
|
100
|
+
default=None,
|
|
101
|
+
description="Optional override for run period length in days",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Invocation method
|
|
105
|
+
invocation_mode: InvocationMode = Field(
|
|
106
|
+
default="cli",
|
|
107
|
+
description="How to invoke EnergyPlus: 'cli' or 'python_api'",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
model_config = {"extra": "forbid"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ==============================================================================
|
|
114
|
+
# EnergyPlus Output Data
|
|
115
|
+
# ==============================================================================
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class EnergyPlusOutputs(BaseModel):
|
|
119
|
+
"""
|
|
120
|
+
EnergyPlus simulation outputs and execution information.
|
|
121
|
+
|
|
122
|
+
This contains detailed, EnergyPlus-specific results from the simulation.
|
|
123
|
+
It's used as the typed 'outputs' field in EnergyPlusOutputEnvelope.
|
|
124
|
+
|
|
125
|
+
## Why This Class Exists
|
|
126
|
+
|
|
127
|
+
The parent envelope (ValidationOutputEnvelope) already has generic fields
|
|
128
|
+
like messages, metrics, and artifacts. This class adds EnergyPlus-specific
|
|
129
|
+
execution details that don't fit the generic schema:
|
|
130
|
+
- Return codes and execution timing
|
|
131
|
+
- Process logs (stdout, stderr, err file)
|
|
132
|
+
- Specific file paths (eplusout.sql, eplusout.err, etc.)
|
|
133
|
+
|
|
134
|
+
## Composing Reusable Models (DRY Principle)
|
|
135
|
+
|
|
136
|
+
We reuse three models from models.py instead of duplicating their definitions:
|
|
137
|
+
- EnergyPlusSimulationOutputs: File paths (SQL, CSV, ERR, ESO)
|
|
138
|
+
- EnergyPlusSimulationMetrics: Extracted metrics (electricity, gas, EUI)
|
|
139
|
+
- EnergyPlusSimulationLogs: Log tails (stdout, stderr, err file)
|
|
140
|
+
|
|
141
|
+
This follows the composition pattern - small, focused models are composed
|
|
142
|
+
into larger structures.
|
|
143
|
+
|
|
144
|
+
## Why Create This Class?
|
|
145
|
+
|
|
146
|
+
The parent envelope (ValidationOutputEnvelope) provides generic validation
|
|
147
|
+
outputs (messages, metrics, artifacts). This class adds EnergyPlus-specific
|
|
148
|
+
execution details that don't fit the generic schema:
|
|
149
|
+
- Process return codes and timing
|
|
150
|
+
- Detailed file paths and logs
|
|
151
|
+
- Invocation method tracking
|
|
152
|
+
|
|
153
|
+
This separation prevents redundancy:
|
|
154
|
+
- Generic validation data → parent envelope fields
|
|
155
|
+
- EnergyPlus execution data → this class
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
# Reuse existing output file tracking from models.py
|
|
159
|
+
outputs: EnergyPlusSimulationOutputs = Field(
|
|
160
|
+
default_factory=EnergyPlusSimulationOutputs,
|
|
161
|
+
description="Paths to EnergyPlus output files (SQL, CSV, ERR, ESO)",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Reuse existing metrics tracking from models.py
|
|
165
|
+
metrics: EnergyPlusSimulationMetrics = Field(
|
|
166
|
+
default_factory=EnergyPlusSimulationMetrics,
|
|
167
|
+
description="Extracted simulation metrics (energy use, etc.)",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Reuse existing log tracking from models.py
|
|
171
|
+
logs: EnergyPlusSimulationLogs | None = Field(
|
|
172
|
+
default=None, description="Simulation logs (stdout, stderr, err file tails)"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Execution metadata
|
|
176
|
+
energyplus_returncode: int = Field(
|
|
177
|
+
description="EnergyPlus process return code (0 = success)"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
execution_seconds: float = Field(
|
|
181
|
+
ge=0, description="Total simulation execution time in seconds"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
invocation_mode: InvocationMode = Field(
|
|
185
|
+
description="How EnergyPlus was invoked ('cli' or 'python_api')"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
model_config = {"extra": "forbid"}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ==============================================================================
|
|
192
|
+
# EnergyPlus-Specific Envelopes
|
|
193
|
+
# ==============================================================================
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class EnergyPlusInputEnvelope(ValidationInputEnvelope):
|
|
197
|
+
"""
|
|
198
|
+
EnergyPlus-specific input envelope.
|
|
199
|
+
|
|
200
|
+
This is what Django serializes and writes to storage as input.json before
|
|
201
|
+
triggering an EnergyPlus validator container.
|
|
202
|
+
|
|
203
|
+
## Type-Safe Field Override
|
|
204
|
+
|
|
205
|
+
The base ValidationInputEnvelope has:
|
|
206
|
+
```python
|
|
207
|
+
inputs: dict[str, Any] = Field(default_factory=dict)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
We override it with a typed version:
|
|
211
|
+
```python
|
|
212
|
+
inputs: EnergyPlusInputs
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
This gives us:
|
|
216
|
+
- Compile-time type checking in Django (mypy catches config errors)
|
|
217
|
+
- Runtime validation (Pydantic validates timesteps, output variables, etc.)
|
|
218
|
+
- Auto-generated documentation (API docs from Pydantic schema)
|
|
219
|
+
- IDE autocomplete when building input envelopes
|
|
220
|
+
|
|
221
|
+
## How Data Flows
|
|
222
|
+
|
|
223
|
+
1. Django creates this envelope:
|
|
224
|
+
- input_files: [IDF with role='primary-model', EPW with role='weather']
|
|
225
|
+
- inputs: EnergyPlusInputs(timestep_per_hour=4)
|
|
226
|
+
- context: ExecutionContext(callback_url=..., execution_bundle_uri=...)
|
|
227
|
+
|
|
228
|
+
2. Django serializes to JSON and uploads to storage as input.json
|
|
229
|
+
|
|
230
|
+
3. Validator container downloads input.json and deserializes to this class
|
|
231
|
+
|
|
232
|
+
4. Validator container extracts typed configuration from inputs field
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
# Override inputs field with typed EnergyPlus configuration
|
|
236
|
+
inputs: EnergyPlusInputs
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class EnergyPlusOutputEnvelope(ValidationOutputEnvelope):
|
|
240
|
+
"""
|
|
241
|
+
EnergyPlus-specific output envelope.
|
|
242
|
+
|
|
243
|
+
This is what the EnergyPlus validator container serializes and writes to
|
|
244
|
+
storage as output.json after simulation completes.
|
|
245
|
+
|
|
246
|
+
## Type-Safe Field Override
|
|
247
|
+
|
|
248
|
+
The base ValidationOutputEnvelope has:
|
|
249
|
+
```python
|
|
250
|
+
outputs: dict[str, Any] | None = Field(default=None)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
We override it with a typed version:
|
|
254
|
+
```python
|
|
255
|
+
outputs: EnergyPlusOutputs
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
This gives us the same benefits as the input envelope (type checking,
|
|
259
|
+
validation, docs, autocomplete).
|
|
260
|
+
|
|
261
|
+
## Generic vs Domain-Specific Data
|
|
262
|
+
|
|
263
|
+
The envelope contains BOTH:
|
|
264
|
+
|
|
265
|
+
**Generic fields** (from parent ValidationOutputEnvelope):
|
|
266
|
+
- messages: ValidationMessage list (errors, warnings - consistent
|
|
267
|
+
across all validators)
|
|
268
|
+
- metrics: ValidationMetric list (high-level computed values for UI display)
|
|
269
|
+
- artifacts: ValidationArtifact list (GCS URIs to important output files)
|
|
270
|
+
- status: SUCCESS/FAILED_VALIDATION/FAILED_RUNTIME/CANCELLED
|
|
271
|
+
|
|
272
|
+
**Domain-specific field** (this class):
|
|
273
|
+
- outputs: EnergyPlusOutputs (returncode, logs, all file paths, execution time)
|
|
274
|
+
|
|
275
|
+
## Why Both?
|
|
276
|
+
|
|
277
|
+
- Generic fields enable consistent UI across all validators
|
|
278
|
+
(same message format, same metric format)
|
|
279
|
+
- Domain-specific outputs preserve detailed execution info
|
|
280
|
+
for debugging and reproduction
|
|
281
|
+
|
|
282
|
+
## How Data Flows
|
|
283
|
+
|
|
284
|
+
1. Validator container runs EnergyPlus simulation
|
|
285
|
+
|
|
286
|
+
2. Validator container creates this envelope:
|
|
287
|
+
- status: SUCCESS or FAILED_VALIDATION
|
|
288
|
+
- messages: [ValidationMessage(severity=ERROR, text="Missing required object")]
|
|
289
|
+
- metrics: [ValidationMetric(name="electricity_kwh", value=12345, unit="kWh")]
|
|
290
|
+
- outputs: EnergyPlusOutputs(energyplus_returncode=0, ...)
|
|
291
|
+
|
|
292
|
+
3. Validator container serializes to JSON and uploads to storage as output.json
|
|
293
|
+
|
|
294
|
+
4. Validator container POSTs minimal callback to Django (run_id, status, result_uri)
|
|
295
|
+
|
|
296
|
+
5. Django downloads output.json and deserializes to this class
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
# Override outputs field with typed EnergyPlus results
|
|
300
|
+
outputs: EnergyPlusOutputs
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reusable EnergyPlus output models.
|
|
3
|
+
|
|
4
|
+
These models represent EnergyPlus simulation outputs and are used as components
|
|
5
|
+
within the typed envelope classes (see envelopes.py).
|
|
6
|
+
|
|
7
|
+
They are kept separate from envelopes to follow the single responsibility principle:
|
|
8
|
+
- These models: What data EnergyPlus produces (files, metrics, logs)
|
|
9
|
+
- Envelope classes: How that data is packaged for Django ↔ validator communication
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path # noqa: TC003
|
|
15
|
+
from typing import Annotated, Literal
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
|
|
19
|
+
# Useful constants for log tailing
|
|
20
|
+
STDOUT_TAIL_CHARS = 4000
|
|
21
|
+
LOG_TAIL_LINES = 200
|
|
22
|
+
|
|
23
|
+
# Type alias for invocation modes
|
|
24
|
+
InvocationMode = Literal["python_api", "cli"]
|
|
25
|
+
|
|
26
|
+
# Type aliases for non-negative numbers
|
|
27
|
+
NonNegFloat = Annotated[float, Field(ge=0)]
|
|
28
|
+
NonNegInt = Annotated[int, Field(ge=0)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EnergyPlusSimulationOutputs(BaseModel):
|
|
32
|
+
"""
|
|
33
|
+
EnergyPlus output file paths.
|
|
34
|
+
|
|
35
|
+
Tracks the standard EnergyPlus output files produced by a simulation.
|
|
36
|
+
Used within EnergyPlusOutputs (in envelopes.py) to preserve file locations.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(extra="forbid")
|
|
40
|
+
eplusout_sql: Path | None = None
|
|
41
|
+
eplusout_err: Path | None = None
|
|
42
|
+
eplusout_csv: Path | None = None
|
|
43
|
+
eplusout_eso: Path | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EnergyPlusSimulationMetrics(BaseModel):
|
|
47
|
+
"""
|
|
48
|
+
Extracted EnergyPlus simulation metrics.
|
|
49
|
+
|
|
50
|
+
These are the core output signals extracted from EnergyPlus simulation results.
|
|
51
|
+
Field names here must match the binding_config["key"] values in the EnergyPlus
|
|
52
|
+
provider catalog (see validibot/validations/providers/energyplus.py).
|
|
53
|
+
|
|
54
|
+
The validator extracts these values from the EnergyPlus SQL database
|
|
55
|
+
(eplusout.sql) and they become available as output signals for assertions.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
model_config = ConfigDict(extra="forbid")
|
|
59
|
+
|
|
60
|
+
# ==========================================================================
|
|
61
|
+
# Energy Consumption (from EnergyPlus meters)
|
|
62
|
+
# ==========================================================================
|
|
63
|
+
|
|
64
|
+
# Total site electricity consumption
|
|
65
|
+
site_electricity_kwh: NonNegFloat | None = None
|
|
66
|
+
|
|
67
|
+
# Total natural gas consumption
|
|
68
|
+
site_natural_gas_kwh: NonNegFloat | None = None
|
|
69
|
+
|
|
70
|
+
# District cooling energy (if present in model)
|
|
71
|
+
site_district_cooling_kwh: NonNegFloat | None = None
|
|
72
|
+
|
|
73
|
+
# District heating energy (if present in model)
|
|
74
|
+
site_district_heating_kwh: NonNegFloat | None = None
|
|
75
|
+
|
|
76
|
+
# ==========================================================================
|
|
77
|
+
# Energy Use Intensity
|
|
78
|
+
# ==========================================================================
|
|
79
|
+
|
|
80
|
+
# Site EUI (total energy / floor area)
|
|
81
|
+
site_eui_kwh_m2: NonNegFloat | None = None
|
|
82
|
+
|
|
83
|
+
# ==========================================================================
|
|
84
|
+
# End-Use Breakdown (all fuels combined)
|
|
85
|
+
# ==========================================================================
|
|
86
|
+
|
|
87
|
+
# Space heating energy
|
|
88
|
+
heating_energy_kwh: NonNegFloat | None = None
|
|
89
|
+
|
|
90
|
+
# Space cooling energy
|
|
91
|
+
cooling_energy_kwh: NonNegFloat | None = None
|
|
92
|
+
|
|
93
|
+
# Interior lighting energy
|
|
94
|
+
interior_lighting_kwh: NonNegFloat | None = None
|
|
95
|
+
|
|
96
|
+
# Fan energy (supply, return, exhaust)
|
|
97
|
+
fans_energy_kwh: NonNegFloat | None = None
|
|
98
|
+
|
|
99
|
+
# Pump energy (chilled water, hot water, condenser)
|
|
100
|
+
pumps_energy_kwh: NonNegFloat | None = None
|
|
101
|
+
|
|
102
|
+
# Domestic hot water energy
|
|
103
|
+
water_systems_kwh: NonNegFloat | None = None
|
|
104
|
+
|
|
105
|
+
# ==========================================================================
|
|
106
|
+
# Comfort / Performance
|
|
107
|
+
# ==========================================================================
|
|
108
|
+
|
|
109
|
+
# Hours heating setpoint not met
|
|
110
|
+
unmet_heating_hours: NonNegFloat | None = None
|
|
111
|
+
|
|
112
|
+
# Hours cooling setpoint not met
|
|
113
|
+
unmet_cooling_hours: NonNegFloat | None = None
|
|
114
|
+
|
|
115
|
+
# Peak electric demand
|
|
116
|
+
peak_electric_demand_w: NonNegFloat | None = None
|
|
117
|
+
|
|
118
|
+
# ==========================================================================
|
|
119
|
+
# Building Characteristics (from IDF/SQL)
|
|
120
|
+
# ==========================================================================
|
|
121
|
+
|
|
122
|
+
# Total conditioned floor area
|
|
123
|
+
floor_area_m2: NonNegFloat | None = None
|
|
124
|
+
|
|
125
|
+
# Number of thermal zones
|
|
126
|
+
zone_count: NonNegInt | None = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class EnergyPlusSimulationLogs(BaseModel):
|
|
130
|
+
"""
|
|
131
|
+
EnergyPlus execution logs.
|
|
132
|
+
|
|
133
|
+
Tails of stdout, stderr, and the eplusout.err file for debugging failed simulations.
|
|
134
|
+
Tails are used instead of full logs to keep envelope sizes reasonable.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
model_config = ConfigDict(extra="forbid")
|
|
138
|
+
stdout_tail: str | None = None
|
|
139
|
+
stderr_tail: str | None = None
|
|
140
|
+
err_tail: str | None = None
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic envelopes for FMI Advanced validator jobs.
|
|
3
|
+
|
|
4
|
+
FMI is an Advanced validator - it runs in a separate container/service rather
|
|
5
|
+
than within the Django app. These schemas define the contract between Django
|
|
6
|
+
and the FMI validator container:
|
|
7
|
+
- Input envelope: FMU URI plus resolved input values and simulation config
|
|
8
|
+
- Output envelope: FMI outputs, metrics, messages, and artifacts
|
|
9
|
+
|
|
10
|
+
Inputs/outputs are keyed by validator catalog slugs. Workflow authors cannot
|
|
11
|
+
remap signals; bindings live on catalog entries (input_binding_path) and
|
|
12
|
+
default to slug-name lookups.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from validibot_shared.validations.envelopes import (
|
|
22
|
+
ExecutionContext,
|
|
23
|
+
InputFileItem,
|
|
24
|
+
SupportedMimeType,
|
|
25
|
+
ValidationInputEnvelope,
|
|
26
|
+
ValidationOutputEnvelope,
|
|
27
|
+
ValidatorType,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FMISimulationConfig(BaseModel):
|
|
32
|
+
"""Simulation configuration for FMI runs."""
|
|
33
|
+
|
|
34
|
+
start_time: float = Field(
|
|
35
|
+
default=0.0,
|
|
36
|
+
description="Simulation start time (seconds).",
|
|
37
|
+
ge=0,
|
|
38
|
+
)
|
|
39
|
+
stop_time: float = Field(
|
|
40
|
+
default=1.0,
|
|
41
|
+
description="Simulation stop time (seconds).",
|
|
42
|
+
gt=0,
|
|
43
|
+
)
|
|
44
|
+
step_size: float = Field(
|
|
45
|
+
default=0.01,
|
|
46
|
+
description="Communication step size (seconds).",
|
|
47
|
+
gt=0,
|
|
48
|
+
)
|
|
49
|
+
tolerance: float | None = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
description="Solver tolerance, if supported by the FMU.",
|
|
52
|
+
gt=0,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FMIInputs(BaseModel):
|
|
57
|
+
"""Resolved inputs plus simulation config, keyed by catalog slugs."""
|
|
58
|
+
|
|
59
|
+
input_values: dict[str, Any] = Field(
|
|
60
|
+
default_factory=dict,
|
|
61
|
+
description="Input values keyed by catalog slugs.",
|
|
62
|
+
)
|
|
63
|
+
simulation: FMISimulationConfig = Field(
|
|
64
|
+
default_factory=FMISimulationConfig,
|
|
65
|
+
description="Simulation time/step configuration.",
|
|
66
|
+
)
|
|
67
|
+
output_variables: list[str] = Field(
|
|
68
|
+
default_factory=list,
|
|
69
|
+
description=(
|
|
70
|
+
"Catalog slugs to capture as outputs. Empty means all output slugs."
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FMIOutputs(BaseModel):
|
|
76
|
+
"""FMI execution results keyed by catalog slugs."""
|
|
77
|
+
|
|
78
|
+
output_values: dict[str, Any] = Field(
|
|
79
|
+
default_factory=dict,
|
|
80
|
+
description="Output values keyed by catalog slugs.",
|
|
81
|
+
)
|
|
82
|
+
fmu_guid: str | None = Field(default=None, description="FMU GUID, if reported.")
|
|
83
|
+
fmi_version: str | None = Field(default=None, description="FMI version.")
|
|
84
|
+
model_name: str | None = Field(default=None, description="FMU model name.")
|
|
85
|
+
execution_seconds: float = Field(
|
|
86
|
+
description="Wall-clock execution time (seconds).",
|
|
87
|
+
ge=0,
|
|
88
|
+
)
|
|
89
|
+
simulation_time_reached: float = Field(
|
|
90
|
+
description="Simulation time reached before completion/stop.",
|
|
91
|
+
ge=0,
|
|
92
|
+
)
|
|
93
|
+
fmu_log: str | None = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="Optional FMU log output.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class FMIInputEnvelope(ValidationInputEnvelope):
|
|
100
|
+
"""Input envelope for FMI validator containers."""
|
|
101
|
+
|
|
102
|
+
inputs: FMIInputs
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FMIOutputEnvelope(ValidationOutputEnvelope):
|
|
106
|
+
"""Output envelope from FMI validator containers.
|
|
107
|
+
|
|
108
|
+
Note: outputs can be None for failure cases where simulation didn't complete.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
outputs: FMIOutputs | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_fmi_input_envelope(
|
|
115
|
+
*,
|
|
116
|
+
run_id: str,
|
|
117
|
+
validator,
|
|
118
|
+
org_id: str,
|
|
119
|
+
org_name: str,
|
|
120
|
+
workflow_id: str,
|
|
121
|
+
step_id: str,
|
|
122
|
+
step_name: str | None,
|
|
123
|
+
fmu_uri: str,
|
|
124
|
+
input_values: dict[str, Any],
|
|
125
|
+
callback_url: str,
|
|
126
|
+
execution_bundle_uri: str,
|
|
127
|
+
simulation: FMISimulationConfig | None = None,
|
|
128
|
+
output_variables: list[str] | None = None,
|
|
129
|
+
) -> FMIInputEnvelope:
|
|
130
|
+
"""
|
|
131
|
+
Build an FMIInputEnvelope from Django validation data.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
run_id: ValidationRun ID
|
|
135
|
+
validator: Validator-like object (id/type/version attrs)
|
|
136
|
+
org_id: Organization ID
|
|
137
|
+
org_name: Organization name
|
|
138
|
+
workflow_id: Workflow ID
|
|
139
|
+
step_id: Workflow step ID
|
|
140
|
+
step_name: Optional step name
|
|
141
|
+
fmu_uri: FMU storage URI (gs://... or local path in dev)
|
|
142
|
+
input_values: Resolved inputs keyed by catalog slug
|
|
143
|
+
callback_url: URL to POST callback
|
|
144
|
+
execution_bundle_uri: Base URI/path for this run's files
|
|
145
|
+
simulation: Optional FMISimulationConfig
|
|
146
|
+
output_variables: Optional list of catalog slugs to capture (empty=all outputs)
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
input_files = [
|
|
150
|
+
InputFileItem(
|
|
151
|
+
name="model.fmu",
|
|
152
|
+
mime_type=SupportedMimeType.FMU,
|
|
153
|
+
role="fmu",
|
|
154
|
+
uri=fmu_uri,
|
|
155
|
+
)
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
envelope_inputs = FMIInputs(
|
|
159
|
+
input_values=input_values,
|
|
160
|
+
simulation=simulation or FMISimulationConfig(),
|
|
161
|
+
output_variables=output_variables or [],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
context = ExecutionContext(
|
|
165
|
+
callback_url=callback_url,
|
|
166
|
+
execution_bundle_uri=execution_bundle_uri,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
envelope = FMIInputEnvelope(
|
|
170
|
+
run_id=run_id,
|
|
171
|
+
validator={
|
|
172
|
+
"id": str(validator.id),
|
|
173
|
+
"type": ValidatorType(validator.validation_type),
|
|
174
|
+
"version": getattr(validator, "version", "1.0.0"),
|
|
175
|
+
},
|
|
176
|
+
org={
|
|
177
|
+
"id": org_id,
|
|
178
|
+
"name": org_name,
|
|
179
|
+
},
|
|
180
|
+
workflow={
|
|
181
|
+
"id": workflow_id,
|
|
182
|
+
"step_id": step_id,
|
|
183
|
+
"step_name": step_name,
|
|
184
|
+
},
|
|
185
|
+
input_files=input_files,
|
|
186
|
+
inputs=envelope_inputs,
|
|
187
|
+
context=context,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return envelope
|