db-process 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.
- db_process/__init__.py +83 -0
- db_process/commands.py +449 -0
- db_process/executable.py +76 -0
- db_process/runner.py +298 -0
- db_process-0.1.0.dist-info/METADATA +18 -0
- db_process-0.1.0.dist-info/RECORD +9 -0
- db_process-0.1.0.dist-info/WHEEL +5 -0
- db_process-0.1.0.dist-info/licenses/LICENSE +21 -0
- db_process-0.1.0.dist-info/top_level.txt +1 -0
db_process/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
db-process
|
|
3
|
+
====================================
|
|
4
|
+
Python interface to DesignBuilder command line processing.
|
|
5
|
+
|
|
6
|
+
This package provides:
|
|
7
|
+
|
|
8
|
+
- **Executable discovery** — find DesignBuilder.exe across env vars,
|
|
9
|
+
default install paths, and the system PATH.
|
|
10
|
+
- **Typed command model** — dataclasses for every ``/process=`` command
|
|
11
|
+
documented in DesignBuilder Help, with a fluent builder API.
|
|
12
|
+
- **Subprocess execution** — blocking and non-blocking modes, timeout
|
|
13
|
+
handling, and idle-detection termination.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from db_process.commands import (
|
|
17
|
+
ChangeAttributeValue,
|
|
18
|
+
ExportAsXML,
|
|
19
|
+
ExternalCommand,
|
|
20
|
+
ImportLibraryData,
|
|
21
|
+
ImportModelData,
|
|
22
|
+
NoClose,
|
|
23
|
+
ProcessChain,
|
|
24
|
+
RunCalculation,
|
|
25
|
+
Screen,
|
|
26
|
+
SimEndDate,
|
|
27
|
+
SimStartDate,
|
|
28
|
+
SwitchScreen,
|
|
29
|
+
TabChange,
|
|
30
|
+
UseSimManager,
|
|
31
|
+
# Convenience factories
|
|
32
|
+
cfd_simulation,
|
|
33
|
+
daylighting,
|
|
34
|
+
eplus_simulation,
|
|
35
|
+
export_xml,
|
|
36
|
+
heating_and_cooling_design,
|
|
37
|
+
sbem_calculation,
|
|
38
|
+
)
|
|
39
|
+
from db_process.executable import find_designbuilder
|
|
40
|
+
from db_process.runner import (
|
|
41
|
+
RunHandle,
|
|
42
|
+
RunResult,
|
|
43
|
+
find_process,
|
|
44
|
+
kill_process,
|
|
45
|
+
kill_when_idle,
|
|
46
|
+
run,
|
|
47
|
+
run_async,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# Executable
|
|
52
|
+
"find_designbuilder",
|
|
53
|
+
# Commands
|
|
54
|
+
"ChangeAttributeValue",
|
|
55
|
+
"ExportAsXML",
|
|
56
|
+
"ExternalCommand",
|
|
57
|
+
"ImportLibraryData",
|
|
58
|
+
"ImportModelData",
|
|
59
|
+
"NoClose",
|
|
60
|
+
"ProcessChain",
|
|
61
|
+
"RunCalculation",
|
|
62
|
+
"Screen",
|
|
63
|
+
"SimEndDate",
|
|
64
|
+
"SimStartDate",
|
|
65
|
+
"SwitchScreen",
|
|
66
|
+
"TabChange",
|
|
67
|
+
"UseSimManager",
|
|
68
|
+
# Convenience factories
|
|
69
|
+
"cfd_simulation",
|
|
70
|
+
"daylighting",
|
|
71
|
+
"eplus_simulation",
|
|
72
|
+
"export_xml",
|
|
73
|
+
"heating_and_cooling_design",
|
|
74
|
+
"sbem_calculation",
|
|
75
|
+
# Runner
|
|
76
|
+
"RunHandle",
|
|
77
|
+
"RunResult",
|
|
78
|
+
"find_process",
|
|
79
|
+
"kill_process",
|
|
80
|
+
"kill_when_idle",
|
|
81
|
+
"run",
|
|
82
|
+
"run_async",
|
|
83
|
+
]
|
db_process/commands.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""
|
|
2
|
+
commands.py
|
|
3
|
+
====================================
|
|
4
|
+
Typed representation of DesignBuilder command line process commands.
|
|
5
|
+
|
|
6
|
+
DesignBuilder CLI syntax::
|
|
7
|
+
|
|
8
|
+
"DesignBuilder.exe" "MyFile.dsb" /process=Command1, Command2, ...
|
|
9
|
+
|
|
10
|
+
This module provides dataclasses for each command type and a
|
|
11
|
+
:class:`ProcessChain` builder to compose them into valid ``/process=`` strings.
|
|
12
|
+
|
|
13
|
+
Reference: DesignBuilder Help > Reference > Command Line Processing
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Optional, Union
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Screen / view commands
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
class Screen(str, Enum):
|
|
28
|
+
"""Available DesignBuilder screen commands."""
|
|
29
|
+
|
|
30
|
+
SIMULATION = "miGSS"
|
|
31
|
+
HEATING_DESIGN = "miGHL"
|
|
32
|
+
COOLING_DESIGN = "miGHG"
|
|
33
|
+
DAYLIGHTING = "miGDY"
|
|
34
|
+
SBEM_DSM = "miGCalculate"
|
|
35
|
+
CFD = "miGCFD"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Individual command dataclasses
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SwitchScreen:
|
|
44
|
+
"""Change the view to a specific DesignBuilder screen.
|
|
45
|
+
|
|
46
|
+
Examples: miGSS, miGHL, miGHG, miGDY, miGCalculate, miGCFD
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
screen: Screen
|
|
50
|
+
|
|
51
|
+
def render(self) -> str:
|
|
52
|
+
return self.screen.value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class RunCalculation:
|
|
57
|
+
"""Run the calculation for the current screen (miTUpdate).
|
|
58
|
+
|
|
59
|
+
Generally this command should be placed after screen switching
|
|
60
|
+
and before any ChangeAttributeValue commands that follow it.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def render(self) -> str:
|
|
64
|
+
return "miTUpdate"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class TabChange:
|
|
69
|
+
"""Change the view to tab *n* on the current screen.
|
|
70
|
+
|
|
71
|
+
Example: ``TabChange_2`` on the Daylighting screen switches to
|
|
72
|
+
the Annual daylighting tab.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
tab: int
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
if self.tab < 1:
|
|
79
|
+
raise ValueError(f"Tab number must be >= 1, got {self.tab}")
|
|
80
|
+
|
|
81
|
+
def render(self) -> str:
|
|
82
|
+
return f"TabChange_{self.tab}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class SimStartDate:
|
|
87
|
+
"""Set simulation start date.
|
|
88
|
+
|
|
89
|
+
Parameters are day and month as integers.
|
|
90
|
+
Example: ``SimStartDate 4 5`` sets start to May 4.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
day: int
|
|
94
|
+
month: int
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
if not (1 <= self.month <= 12):
|
|
98
|
+
raise ValueError(f"Month must be 1-12, got {self.month}")
|
|
99
|
+
if not (1 <= self.day <= 31):
|
|
100
|
+
raise ValueError(f"Day must be 1-31, got {self.day}")
|
|
101
|
+
|
|
102
|
+
def render(self) -> str:
|
|
103
|
+
return f"SimStartDate {self.day} {self.month}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class SimEndDate:
|
|
108
|
+
"""Set simulation end date.
|
|
109
|
+
|
|
110
|
+
Parameters are day and month as integers.
|
|
111
|
+
Example: ``SimEndDate 11 5`` sets end to May 11.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
day: int
|
|
115
|
+
month: int
|
|
116
|
+
|
|
117
|
+
def __post_init__(self) -> None:
|
|
118
|
+
if not (1 <= self.month <= 12):
|
|
119
|
+
raise ValueError(f"Month must be 1-12, got {self.month}")
|
|
120
|
+
if not (1 <= self.day <= 31):
|
|
121
|
+
raise ValueError(f"Day must be 1-31, got {self.day}")
|
|
122
|
+
|
|
123
|
+
def render(self) -> str:
|
|
124
|
+
return f"SimEndDate {self.day} {self.month}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class ChangeAttributeValue:
|
|
129
|
+
"""Change a model attribute at building level.
|
|
130
|
+
|
|
131
|
+
Attribute value types (from documentation):
|
|
132
|
+
- Numeric enums: integer codes (e.g. ``Layout 0``)
|
|
133
|
+
- Booleans: ``1`` for True, ``0`` for False
|
|
134
|
+
- Real numbers: SI units, ``.`` decimal separator
|
|
135
|
+
- Database table selections: numeric Id (obtain via API)
|
|
136
|
+
|
|
137
|
+
Tip: Enable *Show attribute names in tooltips* in
|
|
138
|
+
Tools > Program Options > Interface to find attribute names.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
attribute: str
|
|
142
|
+
value: Union[str, int, float]
|
|
143
|
+
|
|
144
|
+
def render(self) -> str:
|
|
145
|
+
return f"ChangeAttributeValue {self.attribute} {self.value}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class ExternalCommand:
|
|
150
|
+
"""Run an external command with optional arguments.
|
|
151
|
+
|
|
152
|
+
The external executable must be on the system PATH or in the
|
|
153
|
+
working directory.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
command: str
|
|
157
|
+
|
|
158
|
+
def render(self) -> str:
|
|
159
|
+
return f"ExternalCommand_{self.command}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class ImportModelData:
|
|
164
|
+
"""Import model data from a CSV file previously exported from the model."""
|
|
165
|
+
|
|
166
|
+
filepath: str
|
|
167
|
+
|
|
168
|
+
def render(self) -> str:
|
|
169
|
+
return f"ImportModelData_{self.filepath}"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class ImportLibraryData:
|
|
174
|
+
"""Import library data from a file."""
|
|
175
|
+
|
|
176
|
+
filepath: str
|
|
177
|
+
|
|
178
|
+
def render(self) -> str:
|
|
179
|
+
return f"ImportLibraryData_{self.filepath}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class ExportAsXML:
|
|
184
|
+
"""Export the model as a dsbXML file (miFExportAsXML)."""
|
|
185
|
+
|
|
186
|
+
def render(self) -> str:
|
|
187
|
+
return "miFExportAsXML"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class UseSimManager:
|
|
192
|
+
"""Ensure the simulation runs using the Simulation Manager.
|
|
193
|
+
|
|
194
|
+
Uses the Server and EnergyPlus server method settings
|
|
195
|
+
currently configured in the model.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def render(self) -> str:
|
|
199
|
+
return "UseSimManager"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class NoClose:
|
|
204
|
+
"""Prevent DesignBuilder from shutting down after all commands."""
|
|
205
|
+
|
|
206
|
+
def render(self) -> str:
|
|
207
|
+
return "NoClose"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Union of all command types for type hints.
|
|
211
|
+
Command = Union[
|
|
212
|
+
SwitchScreen,
|
|
213
|
+
RunCalculation,
|
|
214
|
+
TabChange,
|
|
215
|
+
SimStartDate,
|
|
216
|
+
SimEndDate,
|
|
217
|
+
ChangeAttributeValue,
|
|
218
|
+
ExternalCommand,
|
|
219
|
+
ImportModelData,
|
|
220
|
+
ImportLibraryData,
|
|
221
|
+
ExportAsXML,
|
|
222
|
+
UseSimManager,
|
|
223
|
+
NoClose,
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# ProcessChain builder
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class ProcessChain:
|
|
233
|
+
"""Build a ``/process=`` command string from a sequence of commands.
|
|
234
|
+
|
|
235
|
+
Usage::
|
|
236
|
+
|
|
237
|
+
chain = (
|
|
238
|
+
ProcessChain()
|
|
239
|
+
.use_sim_manager()
|
|
240
|
+
.switch_screen(Screen.SIMULATION)
|
|
241
|
+
.sim_start_date(1, 1)
|
|
242
|
+
.sim_end_date(31, 12)
|
|
243
|
+
.change_attribute("OccupancyValue", 0.5)
|
|
244
|
+
.run()
|
|
245
|
+
)
|
|
246
|
+
print(chain.to_string())
|
|
247
|
+
# /process=UseSimManager, miGSS, SimStartDate 1 1, SimEndDate 31 12,
|
|
248
|
+
# ChangeAttributeValue OccupancyValue 0.5, miTUpdate
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
commands: list[Command] = field(default_factory=list)
|
|
252
|
+
|
|
253
|
+
# -- Fluent builder methods -----------------------------------------------
|
|
254
|
+
|
|
255
|
+
def add(self, command: Command) -> ProcessChain:
|
|
256
|
+
"""Add an arbitrary command to the chain."""
|
|
257
|
+
self.commands.append(command)
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
def switch_screen(self, screen: Screen) -> ProcessChain:
|
|
261
|
+
"""Add a screen-switching command."""
|
|
262
|
+
return self.add(SwitchScreen(screen))
|
|
263
|
+
|
|
264
|
+
def run(self) -> ProcessChain:
|
|
265
|
+
"""Add miTUpdate (run calculation for current screen)."""
|
|
266
|
+
return self.add(RunCalculation())
|
|
267
|
+
|
|
268
|
+
def tab_change(self, tab: int) -> ProcessChain:
|
|
269
|
+
"""Switch to a specific tab on the current screen."""
|
|
270
|
+
return self.add(TabChange(tab))
|
|
271
|
+
|
|
272
|
+
def sim_start_date(self, day: int, month: int) -> ProcessChain:
|
|
273
|
+
"""Set simulation start date."""
|
|
274
|
+
return self.add(SimStartDate(day, month))
|
|
275
|
+
|
|
276
|
+
def sim_end_date(self, day: int, month: int) -> ProcessChain:
|
|
277
|
+
"""Set simulation end date."""
|
|
278
|
+
return self.add(SimEndDate(day, month))
|
|
279
|
+
|
|
280
|
+
def change_attribute(
|
|
281
|
+
self, attribute: str, value: Union[str, int, float]
|
|
282
|
+
) -> ProcessChain:
|
|
283
|
+
"""Change a model attribute value."""
|
|
284
|
+
return self.add(ChangeAttributeValue(attribute, value))
|
|
285
|
+
|
|
286
|
+
def external_command(self, command: str) -> ProcessChain:
|
|
287
|
+
"""Run an external command."""
|
|
288
|
+
return self.add(ExternalCommand(command))
|
|
289
|
+
|
|
290
|
+
def import_model_data(self, filepath: str) -> ProcessChain:
|
|
291
|
+
"""Import model data from a CSV file."""
|
|
292
|
+
return self.add(ImportModelData(filepath))
|
|
293
|
+
|
|
294
|
+
def import_library_data(self, filepath: str) -> ProcessChain:
|
|
295
|
+
"""Import library data from a file."""
|
|
296
|
+
return self.add(ImportLibraryData(filepath))
|
|
297
|
+
|
|
298
|
+
def export_as_xml(self) -> ProcessChain:
|
|
299
|
+
"""Export the model as dsbXML."""
|
|
300
|
+
return self.add(ExportAsXML())
|
|
301
|
+
|
|
302
|
+
def use_sim_manager(self) -> ProcessChain:
|
|
303
|
+
"""Use the Simulation Manager for the run."""
|
|
304
|
+
return self.add(UseSimManager())
|
|
305
|
+
|
|
306
|
+
def no_close(self) -> ProcessChain:
|
|
307
|
+
"""Prevent DesignBuilder from closing after processing."""
|
|
308
|
+
return self.add(NoClose())
|
|
309
|
+
|
|
310
|
+
# -- Rendering ------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def to_string(self) -> str:
|
|
313
|
+
"""Render the chain as a ``/process=...`` argument string.
|
|
314
|
+
|
|
315
|
+
Raises
|
|
316
|
+
------
|
|
317
|
+
ValueError
|
|
318
|
+
If the chain contains no commands.
|
|
319
|
+
"""
|
|
320
|
+
if not self.commands:
|
|
321
|
+
raise ValueError("ProcessChain is empty — add at least one command.")
|
|
322
|
+
parts = [cmd.render() for cmd in self.commands]
|
|
323
|
+
return "/process=" + ", ".join(parts)
|
|
324
|
+
|
|
325
|
+
def to_list(self) -> list[str]:
|
|
326
|
+
"""Return the chain as a list of individual command strings."""
|
|
327
|
+
return [cmd.render() for cmd in self.commands]
|
|
328
|
+
|
|
329
|
+
def __str__(self) -> str:
|
|
330
|
+
return self.to_string()
|
|
331
|
+
|
|
332
|
+
def __repr__(self) -> str:
|
|
333
|
+
return f"ProcessChain({self.commands!r})"
|
|
334
|
+
|
|
335
|
+
def __len__(self) -> int:
|
|
336
|
+
return len(self.commands)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# Convenience factory functions
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
def eplus_simulation(
|
|
344
|
+
*,
|
|
345
|
+
sim_start_date: Optional[tuple[int, int]] = None,
|
|
346
|
+
sim_end_date: Optional[tuple[int, int]] = None,
|
|
347
|
+
use_sim_manager: bool = False,
|
|
348
|
+
attributes: Optional[list[tuple[str, Union[str, int, float]]]] = None,
|
|
349
|
+
no_close: bool = False,
|
|
350
|
+
) -> ProcessChain:
|
|
351
|
+
"""Build a standard EnergyPlus simulation command chain.
|
|
352
|
+
|
|
353
|
+
This produces the equivalent of what ``db-batch`` creates for
|
|
354
|
+
``analysis_type="eplus"``.
|
|
355
|
+
"""
|
|
356
|
+
chain = ProcessChain()
|
|
357
|
+
|
|
358
|
+
if use_sim_manager:
|
|
359
|
+
chain.use_sim_manager()
|
|
360
|
+
|
|
361
|
+
if sim_start_date:
|
|
362
|
+
chain.sim_start_date(sim_start_date[0], sim_start_date[1])
|
|
363
|
+
|
|
364
|
+
if sim_end_date:
|
|
365
|
+
chain.sim_end_date(sim_end_date[0], sim_end_date[1])
|
|
366
|
+
|
|
367
|
+
if attributes:
|
|
368
|
+
for attr, val in attributes:
|
|
369
|
+
chain.change_attribute(attr, val)
|
|
370
|
+
|
|
371
|
+
chain.switch_screen(Screen.SIMULATION)
|
|
372
|
+
chain.run()
|
|
373
|
+
|
|
374
|
+
if no_close:
|
|
375
|
+
chain.no_close()
|
|
376
|
+
|
|
377
|
+
return chain
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def sbem_calculation(
|
|
381
|
+
*,
|
|
382
|
+
sim_start_date: Optional[tuple[int, int]] = None,
|
|
383
|
+
sim_end_date: Optional[tuple[int, int]] = None,
|
|
384
|
+
attributes: Optional[list[tuple[str, Union[str, int, float]]]] = None,
|
|
385
|
+
no_close: bool = False,
|
|
386
|
+
) -> ProcessChain:
|
|
387
|
+
"""Build a standard SBEM/DSM calculation command chain."""
|
|
388
|
+
chain = ProcessChain()
|
|
389
|
+
|
|
390
|
+
if sim_start_date:
|
|
391
|
+
chain.sim_start_date(sim_start_date[0], sim_start_date[1])
|
|
392
|
+
|
|
393
|
+
if sim_end_date:
|
|
394
|
+
chain.sim_end_date(sim_end_date[0], sim_end_date[1])
|
|
395
|
+
|
|
396
|
+
if attributes:
|
|
397
|
+
for attr, val in attributes:
|
|
398
|
+
chain.change_attribute(attr, val)
|
|
399
|
+
|
|
400
|
+
chain.switch_screen(Screen.SBEM_DSM)
|
|
401
|
+
chain.run()
|
|
402
|
+
|
|
403
|
+
if no_close:
|
|
404
|
+
chain.no_close()
|
|
405
|
+
|
|
406
|
+
return chain
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def export_xml() -> ProcessChain:
|
|
410
|
+
"""Build a command chain to export the model as dsbXML."""
|
|
411
|
+
return ProcessChain().export_as_xml()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def heating_and_cooling_design(*, no_close: bool = False) -> ProcessChain:
|
|
415
|
+
"""Run both Heating and Cooling Design calculations."""
|
|
416
|
+
chain = (
|
|
417
|
+
ProcessChain()
|
|
418
|
+
.switch_screen(Screen.HEATING_DESIGN)
|
|
419
|
+
.run()
|
|
420
|
+
.switch_screen(Screen.COOLING_DESIGN)
|
|
421
|
+
.run()
|
|
422
|
+
)
|
|
423
|
+
if no_close:
|
|
424
|
+
chain.no_close()
|
|
425
|
+
return chain
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def daylighting(
|
|
429
|
+
*, run_annual: bool = False, no_close: bool = False
|
|
430
|
+
) -> ProcessChain:
|
|
431
|
+
"""Run daylighting calculation, optionally including annual daylighting.
|
|
432
|
+
|
|
433
|
+
When *run_annual* is True, runs both Illuminance (tab 1) and
|
|
434
|
+
Annual daylighting (tab 2) calculations.
|
|
435
|
+
"""
|
|
436
|
+
chain = ProcessChain().switch_screen(Screen.DAYLIGHTING).run()
|
|
437
|
+
if run_annual:
|
|
438
|
+
chain.tab_change(2).run()
|
|
439
|
+
if no_close:
|
|
440
|
+
chain.no_close()
|
|
441
|
+
return chain
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def cfd_simulation(*, no_close: bool = False) -> ProcessChain:
|
|
445
|
+
"""Run a CFD simulation."""
|
|
446
|
+
chain = ProcessChain().switch_screen(Screen.CFD).run()
|
|
447
|
+
if no_close:
|
|
448
|
+
chain.no_close()
|
|
449
|
+
return chain
|
db_process/executable.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
executable.py
|
|
3
|
+
====================================
|
|
4
|
+
Locate and validate the DesignBuilder executable.
|
|
5
|
+
|
|
6
|
+
Resolution order:
|
|
7
|
+
1. Explicit path argument
|
|
8
|
+
2. DESIGNBUILDER_EXE environment variable
|
|
9
|
+
3. Common Windows installation directories
|
|
10
|
+
4. System PATH (via shutil.which)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
# Environment variable to override the DesignBuilder executable path.
|
|
19
|
+
ENV_VAR = "DESIGNBUILDER_EXE"
|
|
20
|
+
|
|
21
|
+
# Common DesignBuilder installation paths (Windows).
|
|
22
|
+
DEFAULT_INSTALL_PATHS = [
|
|
23
|
+
Path(r"C:\Program Files (x86)\DesignBuilder\DesignBuilder.exe"),
|
|
24
|
+
Path(r"C:\Program Files\DesignBuilder\DesignBuilder.exe"),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_designbuilder(exe_path: Optional[str | Path] = None) -> Path:
|
|
29
|
+
"""Locate the DesignBuilder executable.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
exe_path : str or Path, optional
|
|
34
|
+
Explicit path to DesignBuilder.exe. Takes highest priority.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
Path
|
|
39
|
+
Resolved path to the executable.
|
|
40
|
+
|
|
41
|
+
Raises
|
|
42
|
+
------
|
|
43
|
+
FileNotFoundError
|
|
44
|
+
If DesignBuilder cannot be found via any method.
|
|
45
|
+
"""
|
|
46
|
+
# 1. Explicit argument
|
|
47
|
+
if exe_path is not None:
|
|
48
|
+
p = Path(exe_path)
|
|
49
|
+
if p.is_file():
|
|
50
|
+
return p
|
|
51
|
+
raise FileNotFoundError(f"DesignBuilder not found at: {p}")
|
|
52
|
+
|
|
53
|
+
# 2. Environment variable
|
|
54
|
+
env = os.environ.get(ENV_VAR)
|
|
55
|
+
if env:
|
|
56
|
+
p = Path(env)
|
|
57
|
+
if p.is_file():
|
|
58
|
+
return p
|
|
59
|
+
raise FileNotFoundError(
|
|
60
|
+
f"{ENV_VAR} is set to '{env}' but the file does not exist."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# 3. Common install locations
|
|
64
|
+
for p in DEFAULT_INSTALL_PATHS:
|
|
65
|
+
if p.is_file():
|
|
66
|
+
return p
|
|
67
|
+
|
|
68
|
+
# 4. System PATH
|
|
69
|
+
which = shutil.which("DesignBuilder")
|
|
70
|
+
if which:
|
|
71
|
+
return Path(which)
|
|
72
|
+
|
|
73
|
+
raise FileNotFoundError(
|
|
74
|
+
"Could not find DesignBuilder.exe. "
|
|
75
|
+
f"Set the {ENV_VAR} environment variable or pass the path explicitly."
|
|
76
|
+
)
|
db_process/runner.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
runner.py
|
|
3
|
+
====================================
|
|
4
|
+
Run DesignBuilder as a subprocess with support for blocking and
|
|
5
|
+
non-blocking execution, timeouts, and idle-detection termination.
|
|
6
|
+
|
|
7
|
+
This module unifies the execution patterns from ``designbuilder_schema``
|
|
8
|
+
(blocking with error capture) and ``db-batch`` (Popen + idle monitoring).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional, Union
|
|
18
|
+
|
|
19
|
+
import psutil
|
|
20
|
+
|
|
21
|
+
from db_process.commands import ProcessChain
|
|
22
|
+
from db_process.executable import find_designbuilder
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Result dataclass
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class RunResult:
|
|
31
|
+
"""Outcome of a DesignBuilder process execution."""
|
|
32
|
+
|
|
33
|
+
success: bool
|
|
34
|
+
"""Whether the process completed without error."""
|
|
35
|
+
|
|
36
|
+
returncode: Optional[int] = None
|
|
37
|
+
"""Process return code (None if killed or not yet finished)."""
|
|
38
|
+
|
|
39
|
+
stdout: str = ""
|
|
40
|
+
"""Captured stdout (blocking mode only)."""
|
|
41
|
+
|
|
42
|
+
stderr: str = ""
|
|
43
|
+
"""Captured stderr (blocking mode only)."""
|
|
44
|
+
|
|
45
|
+
timed_out: bool = False
|
|
46
|
+
"""True if the process was killed due to timeout."""
|
|
47
|
+
|
|
48
|
+
killed_idle: bool = False
|
|
49
|
+
"""True if the process was killed by idle detection."""
|
|
50
|
+
|
|
51
|
+
duration_seconds: Optional[float] = None
|
|
52
|
+
"""Wall-clock duration of the run in seconds."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Process management
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
DESIGNBUILDER_PROCESS_NAME = "DesignBuilder.exe"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def find_process(name: str = DESIGNBUILDER_PROCESS_NAME) -> Optional[psutil.Process]:
|
|
63
|
+
"""Find a running process by name.
|
|
64
|
+
|
|
65
|
+
Returns the first matching process, or None.
|
|
66
|
+
"""
|
|
67
|
+
for p in psutil.process_iter(["name"]):
|
|
68
|
+
try:
|
|
69
|
+
if p.info["name"] == name:
|
|
70
|
+
return p
|
|
71
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
72
|
+
continue
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def kill_process(name: str = DESIGNBUILDER_PROCESS_NAME) -> bool:
|
|
77
|
+
"""Terminate a DesignBuilder process forcefully.
|
|
78
|
+
|
|
79
|
+
Returns True if a process was found and killed, False otherwise.
|
|
80
|
+
"""
|
|
81
|
+
proc = find_process(name)
|
|
82
|
+
if proc is None:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
proc.kill()
|
|
86
|
+
proc.wait(timeout=5)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def kill_when_idle(
|
|
91
|
+
name: str = DESIGNBUILDER_PROCESS_NAME,
|
|
92
|
+
*,
|
|
93
|
+
idle_threshold: float = 10,
|
|
94
|
+
check_interval: float = 0.5,
|
|
95
|
+
startup_period: float = 20,
|
|
96
|
+
cpu_threshold: float = 0.1,
|
|
97
|
+
) -> bool:
|
|
98
|
+
"""Monitor a process and kill it once CPU usage stays below threshold.
|
|
99
|
+
|
|
100
|
+
The function blocks until the process is killed or exits on its own.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
name : str
|
|
105
|
+
Process name to monitor.
|
|
106
|
+
idle_threshold : float
|
|
107
|
+
Seconds the process must be idle before termination.
|
|
108
|
+
check_interval : float
|
|
109
|
+
Polling interval in seconds.
|
|
110
|
+
startup_period : float
|
|
111
|
+
Grace period before idle detection begins.
|
|
112
|
+
cpu_threshold : float
|
|
113
|
+
CPU percentage below which the process is considered idle.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
bool
|
|
118
|
+
True if the process was killed due to idle, False if it
|
|
119
|
+
exited on its own or was not found.
|
|
120
|
+
"""
|
|
121
|
+
proc = find_process(name)
|
|
122
|
+
if proc is None:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
idle_time = 0.0
|
|
126
|
+
has_been_active = False
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if startup_period > 0:
|
|
130
|
+
time.sleep(startup_period)
|
|
131
|
+
|
|
132
|
+
while proc.is_running():
|
|
133
|
+
cpu = proc.cpu_percent(interval=0.1)
|
|
134
|
+
|
|
135
|
+
if cpu >= cpu_threshold:
|
|
136
|
+
has_been_active = True
|
|
137
|
+
idle_time = 0.0
|
|
138
|
+
elif has_been_active:
|
|
139
|
+
idle_time += check_interval
|
|
140
|
+
if idle_time >= idle_threshold:
|
|
141
|
+
proc.kill()
|
|
142
|
+
proc.wait(timeout=5)
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
time.sleep(check_interval)
|
|
146
|
+
|
|
147
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Blocking execution
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def run(
|
|
158
|
+
model_path: Union[str, Path],
|
|
159
|
+
process_chain: Optional[ProcessChain] = None,
|
|
160
|
+
*,
|
|
161
|
+
exe_path: Optional[Union[str, Path]] = None,
|
|
162
|
+
timeout: Optional[int] = None,
|
|
163
|
+
) -> RunResult:
|
|
164
|
+
"""Run DesignBuilder in blocking mode and wait for completion.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
model_path : str or Path
|
|
169
|
+
Path to the .dsb model file.
|
|
170
|
+
process_chain : ProcessChain, optional
|
|
171
|
+
Commands to execute. When None, DesignBuilder opens the file
|
|
172
|
+
without processing (useful for XML import).
|
|
173
|
+
exe_path : str or Path, optional
|
|
174
|
+
Explicit path to DesignBuilder.exe.
|
|
175
|
+
timeout : int, optional
|
|
176
|
+
Maximum seconds to wait. None means wait indefinitely.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
RunResult
|
|
181
|
+
Outcome of the execution.
|
|
182
|
+
"""
|
|
183
|
+
exe = find_designbuilder(exe_path)
|
|
184
|
+
model = Path(model_path)
|
|
185
|
+
|
|
186
|
+
cmd = [str(exe), str(model)]
|
|
187
|
+
if process_chain is not None:
|
|
188
|
+
cmd.append(process_chain.to_string())
|
|
189
|
+
|
|
190
|
+
t0 = time.monotonic()
|
|
191
|
+
try:
|
|
192
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
193
|
+
duration = time.monotonic() - t0
|
|
194
|
+
return RunResult(
|
|
195
|
+
success=result.returncode == 0,
|
|
196
|
+
returncode=result.returncode,
|
|
197
|
+
stdout=result.stdout,
|
|
198
|
+
stderr=result.stderr,
|
|
199
|
+
duration_seconds=duration,
|
|
200
|
+
)
|
|
201
|
+
except subprocess.TimeoutExpired:
|
|
202
|
+
duration = time.monotonic() - t0
|
|
203
|
+
kill_process()
|
|
204
|
+
return RunResult(
|
|
205
|
+
success=False,
|
|
206
|
+
timed_out=True,
|
|
207
|
+
duration_seconds=duration,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Non-blocking execution
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class RunHandle:
|
|
217
|
+
"""Handle for a non-blocking DesignBuilder process.
|
|
218
|
+
|
|
219
|
+
Use :meth:`wait`, :meth:`kill`, or :meth:`kill_when_idle` to
|
|
220
|
+
manage the process after launch.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
process: subprocess.Popen
|
|
224
|
+
"""The underlying Popen object."""
|
|
225
|
+
|
|
226
|
+
start_time: float = field(default_factory=time.monotonic)
|
|
227
|
+
"""Monotonic timestamp when the process was launched."""
|
|
228
|
+
|
|
229
|
+
def is_running(self) -> bool:
|
|
230
|
+
"""Check if the process is still running."""
|
|
231
|
+
return self.process.poll() is None
|
|
232
|
+
|
|
233
|
+
def wait(self, timeout: Optional[int] = None) -> RunResult:
|
|
234
|
+
"""Block until the process finishes or timeout expires."""
|
|
235
|
+
try:
|
|
236
|
+
self.process.wait(timeout=timeout)
|
|
237
|
+
duration = time.monotonic() - self.start_time
|
|
238
|
+
return RunResult(
|
|
239
|
+
success=self.process.returncode == 0,
|
|
240
|
+
returncode=self.process.returncode,
|
|
241
|
+
duration_seconds=duration,
|
|
242
|
+
)
|
|
243
|
+
except subprocess.TimeoutExpired:
|
|
244
|
+
duration = time.monotonic() - self.start_time
|
|
245
|
+
kill_process()
|
|
246
|
+
return RunResult(
|
|
247
|
+
success=False,
|
|
248
|
+
timed_out=True,
|
|
249
|
+
duration_seconds=duration,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def kill(self) -> None:
|
|
253
|
+
"""Terminate the process immediately."""
|
|
254
|
+
kill_process()
|
|
255
|
+
|
|
256
|
+
def kill_when_idle(self, **kwargs) -> bool:
|
|
257
|
+
"""Monitor and kill the process when it becomes idle.
|
|
258
|
+
|
|
259
|
+
Accepts the same keyword arguments as :func:`kill_when_idle`.
|
|
260
|
+
This call blocks until the process is killed or exits.
|
|
261
|
+
"""
|
|
262
|
+
return kill_when_idle(**kwargs)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def run_async(
|
|
266
|
+
model_path: Union[str, Path],
|
|
267
|
+
process_chain: Optional[ProcessChain] = None,
|
|
268
|
+
*,
|
|
269
|
+
exe_path: Optional[Union[str, Path]] = None,
|
|
270
|
+
) -> RunHandle:
|
|
271
|
+
"""Launch DesignBuilder in non-blocking mode.
|
|
272
|
+
|
|
273
|
+
Returns a :class:`RunHandle` that can be used to wait for
|
|
274
|
+
completion, kill the process, or monitor for idle termination.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
model_path : str or Path
|
|
279
|
+
Path to the .dsb model file.
|
|
280
|
+
process_chain : ProcessChain, optional
|
|
281
|
+
Commands to execute.
|
|
282
|
+
exe_path : str or Path, optional
|
|
283
|
+
Explicit path to DesignBuilder.exe.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
RunHandle
|
|
288
|
+
Handle for the launched process.
|
|
289
|
+
"""
|
|
290
|
+
exe = find_designbuilder(exe_path)
|
|
291
|
+
model = Path(model_path)
|
|
292
|
+
|
|
293
|
+
cmd = [str(exe), str(model)]
|
|
294
|
+
if process_chain is not None:
|
|
295
|
+
cmd.append(process_chain.to_string())
|
|
296
|
+
|
|
297
|
+
proc = subprocess.Popen(cmd)
|
|
298
|
+
return RunHandle(process=proc)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: db-process
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python interface to DesignBuilder command line processing.
|
|
5
|
+
Author-email: Bartlomiej Tokarzewski <bartlomiej.tokarzewski@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/tokarzewski/db-process
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/tokarzewski/db-process/issues
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: psutil>=5.9.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# db-process
|
|
18
|
+
DesignBuilder command line processing
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
db_process/__init__.py,sha256=Zf8vIjye2ejV3CZJP1HUyCA5F8dSIrm14_eMcoTgKLk,1822
|
|
2
|
+
db_process/commands.py,sha256=1E1EMxkx8lWLBbD5z4hZFEMRZV8XGbFU2xFzX92aXlY,12470
|
|
3
|
+
db_process/executable.py,sha256=0nQ38-x6OWUpd11ac3DRziwX8mHMz5xtI9xOyTXauAY,1957
|
|
4
|
+
db_process/runner.py,sha256=HMWbykseGAuKSgtmsIFv1xjRWI69QD0d8p5PwgmrCiQ,8633
|
|
5
|
+
db_process-0.1.0.dist-info/licenses/LICENSE,sha256=Dik23IwCfTKw3e2cSounRZn27EeAI421gnZ9vzgWdJw,1088
|
|
6
|
+
db_process-0.1.0.dist-info/METADATA,sha256=WHV8vRgg4EI5lV5v7jw3vk9flDBBaa0orFkoknHxRFA,635
|
|
7
|
+
db_process-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
db_process-0.1.0.dist-info/top_level.txt,sha256=yOsVAh7uYY7yuQvrtzeyg05CLpYbNjePhNn1V16XloI,11
|
|
9
|
+
db_process-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tokarzewski
|
|
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 @@
|
|
|
1
|
+
db_process
|