honeybee-core 1.64.12__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.
- honeybee/__init__.py +23 -0
- honeybee/__main__.py +4 -0
- honeybee/_base.py +331 -0
- honeybee/_basewithshade.py +310 -0
- honeybee/_lockable.py +99 -0
- honeybee/altnumber.py +47 -0
- honeybee/aperture.py +997 -0
- honeybee/boundarycondition.py +358 -0
- honeybee/checkdup.py +173 -0
- honeybee/cli/__init__.py +118 -0
- honeybee/cli/compare.py +132 -0
- honeybee/cli/create.py +265 -0
- honeybee/cli/edit.py +559 -0
- honeybee/cli/lib.py +103 -0
- honeybee/cli/setconfig.py +43 -0
- honeybee/cli/validate.py +224 -0
- honeybee/colorobj.py +363 -0
- honeybee/config.json +5 -0
- honeybee/config.py +347 -0
- honeybee/dictutil.py +54 -0
- honeybee/door.py +746 -0
- honeybee/extensionutil.py +208 -0
- honeybee/face.py +2360 -0
- honeybee/facetype.py +153 -0
- honeybee/logutil.py +79 -0
- honeybee/model.py +4272 -0
- honeybee/orientation.py +132 -0
- honeybee/properties.py +845 -0
- honeybee/room.py +3485 -0
- honeybee/search.py +107 -0
- honeybee/shade.py +514 -0
- honeybee/shademesh.py +362 -0
- honeybee/typing.py +498 -0
- honeybee/units.py +88 -0
- honeybee/writer/__init__.py +7 -0
- honeybee/writer/aperture.py +6 -0
- honeybee/writer/door.py +6 -0
- honeybee/writer/face.py +6 -0
- honeybee/writer/model.py +6 -0
- honeybee/writer/room.py +6 -0
- honeybee/writer/shade.py +6 -0
- honeybee/writer/shademesh.py +6 -0
- honeybee_core-1.64.12.dist-info/METADATA +94 -0
- honeybee_core-1.64.12.dist-info/RECORD +48 -0
- honeybee_core-1.64.12.dist-info/WHEEL +5 -0
- honeybee_core-1.64.12.dist-info/entry_points.txt +2 -0
- honeybee_core-1.64.12.dist-info/licenses/LICENSE +661 -0
- honeybee_core-1.64.12.dist-info/top_level.txt +1 -0
honeybee/cli/edit.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""honeybee model editing commands."""
|
|
2
|
+
import click
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ladybug_geometry.geometry2d.pointvector import Vector2D
|
|
8
|
+
from ladybug_geometry.geometry3d.pointvector import Vector3D
|
|
9
|
+
|
|
10
|
+
from honeybee.model import Model
|
|
11
|
+
from honeybee.units import parse_distance_string
|
|
12
|
+
from honeybee.facetype import Wall
|
|
13
|
+
from honeybee.boundarycondition import Outdoors
|
|
14
|
+
from honeybee.boundarycondition import boundary_conditions as bcs
|
|
15
|
+
try:
|
|
16
|
+
ad_bc = bcs.adiabatic
|
|
17
|
+
except AttributeError: # honeybee_energy is not loaded and adiabatic does not exist
|
|
18
|
+
ad_bc = None
|
|
19
|
+
|
|
20
|
+
_logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group(help='Commands for editing Honeybee models.')
|
|
24
|
+
def edit():
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@edit.command('convert-units')
|
|
29
|
+
@click.argument('model-file', type=click.Path(
|
|
30
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
31
|
+
@click.argument('units', type=str)
|
|
32
|
+
@click.option('--scale/--do-not-scale', ' /-ns', help='Flag to note whether the model '
|
|
33
|
+
'should be scaled as it is converted to the new units system.',
|
|
34
|
+
default=True, show_default=True)
|
|
35
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
36
|
+
' with solved adjacency. By default it will be printed out to stdout',
|
|
37
|
+
type=click.File('w'), default='-')
|
|
38
|
+
def convert_units(model_file, units, scale, output_file):
|
|
39
|
+
"""Convert a Model to a given units system.
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
Args:
|
|
43
|
+
model_file: Full path to a Honeybee Model file.
|
|
44
|
+
units: Text for the units system to which the model will be converted.
|
|
45
|
+
Choose from (Meters, Millimeters, Feet, Inches, Centimeters).
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
parsed_model = Model.from_file(model_file)
|
|
49
|
+
if scale:
|
|
50
|
+
parsed_model.convert_to_units(units)
|
|
51
|
+
else:
|
|
52
|
+
parsed_model.units = units
|
|
53
|
+
# write the new model out to the file or stdout
|
|
54
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
55
|
+
except Exception as e:
|
|
56
|
+
_logger.exception('Model unit conversion failed.\n{}'.format(e))
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
else:
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@edit.command('solve-adjacency')
|
|
63
|
+
@click.argument('model-file', type=click.Path(
|
|
64
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
65
|
+
@click.option('--no-merge/--merge-coplanar', ' /-m', help='Flag to note whether '
|
|
66
|
+
'coplanar Faces of the Rooms should be merged before proceeding with '
|
|
67
|
+
'the rest of the adjacency solving. This is particularly helpful when '
|
|
68
|
+
'used with the --intersect option since it will ensure the Room geometry '
|
|
69
|
+
'is relatively clean before the intersection and adjacency solving '
|
|
70
|
+
'occurs.', default=True, show_default=True)
|
|
71
|
+
@click.option('--no-intersect/--intersect', ' /-i', help='Flag to note whether the '
|
|
72
|
+
'Faces of the Rooms should be intersected with one another before '
|
|
73
|
+
'the adjacencies are solved.', default=True, show_default=True)
|
|
74
|
+
@click.option('--no-overwrite/--overwrite', ' /-ow', help='Flag to note whether existing'
|
|
75
|
+
' Surface boundary conditions should be overwritten.',
|
|
76
|
+
default=True, show_default=True)
|
|
77
|
+
@click.option('--wall/--air-boundary', ' /-ab', help='Flag to note whether the '
|
|
78
|
+
'wall adjacencies should be of the air boundary face type.',
|
|
79
|
+
default=True, show_default=True)
|
|
80
|
+
@click.option('--surface/--adiabatic', ' /-a', help='Flag to note whether the '
|
|
81
|
+
'adjacencies should be surface or adiabatic.',
|
|
82
|
+
default=True, show_default=True)
|
|
83
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
84
|
+
' with solved adjacency. By default it will be printed out to stdout',
|
|
85
|
+
type=click.File('w'), default='-')
|
|
86
|
+
def solve_adjacency(model_file, no_merge, no_intersect, no_overwrite,
|
|
87
|
+
wall, surface, output_file):
|
|
88
|
+
"""Solve adjacency between Rooms of a Model file.
|
|
89
|
+
|
|
90
|
+
\b
|
|
91
|
+
Args:
|
|
92
|
+
model_file: Full path to a Honeybee Model file.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
# serialize the Model to Python and check the tolerance
|
|
96
|
+
parsed_model = Model.from_file(model_file)
|
|
97
|
+
assert parsed_model.tolerance != 0, \
|
|
98
|
+
'Model must have a non-zero tolerance to use solve-adjacency.'
|
|
99
|
+
|
|
100
|
+
# solve adjacency
|
|
101
|
+
merge_coplanar = not no_merge
|
|
102
|
+
intersect = not no_intersect
|
|
103
|
+
overwrite = not no_overwrite
|
|
104
|
+
air_boundary = not wall
|
|
105
|
+
adiabatic = not surface
|
|
106
|
+
parsed_model.solve_adjacency(
|
|
107
|
+
merge_coplanar, intersect, overwrite,
|
|
108
|
+
air_boundary=air_boundary, adiabatic=adiabatic)
|
|
109
|
+
|
|
110
|
+
# write the new model out to the file or stdout
|
|
111
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
112
|
+
except Exception as e:
|
|
113
|
+
_logger.exception('Model solve adjacency failed.\n{}'.format(e))
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
else:
|
|
116
|
+
sys.exit(0)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@edit.command('windows-by-ratio')
|
|
120
|
+
@click.argument('model-file', type=click.Path(
|
|
121
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
122
|
+
@click.argument('ratio', type=float)
|
|
123
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
124
|
+
' with windows. By default it will be printed out to stdout',
|
|
125
|
+
type=click.File('w'), default='-')
|
|
126
|
+
def windows_by_ratio(model_file, ratio, output_file):
|
|
127
|
+
"""Add apertures to all outdoor walls of a model given a ratio.
|
|
128
|
+
|
|
129
|
+
Note that this method removes any existing apertures and doors from the Walls.
|
|
130
|
+
This method attempts to generate as few apertures as necessary to meet the ratio.
|
|
131
|
+
|
|
132
|
+
\b
|
|
133
|
+
Args:
|
|
134
|
+
model_file: Full path to a Honeybee Model file.
|
|
135
|
+
ratio: A number between 0 and 1 (but not perfectly equal to 1)
|
|
136
|
+
for the desired ratio between window area and wall area.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
# serialize the Model and check the Model tolerance
|
|
140
|
+
parsed_model = Model.from_file(model_file)
|
|
141
|
+
assert parsed_model.tolerance != 0, \
|
|
142
|
+
'Model must have a non-zero tolerance to use windows-by-ratio.'
|
|
143
|
+
tol = parsed_model.tolerance
|
|
144
|
+
|
|
145
|
+
# generate the windows for all walls of rooms
|
|
146
|
+
for room in parsed_model.rooms:
|
|
147
|
+
for face in room.faces:
|
|
148
|
+
if isinstance(face.boundary_condition, Outdoors) and \
|
|
149
|
+
isinstance(face.type, Wall):
|
|
150
|
+
face.apertures_by_ratio(ratio, tol)
|
|
151
|
+
|
|
152
|
+
# write the new model out to the file or stdout
|
|
153
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
154
|
+
except Exception as e:
|
|
155
|
+
_logger.exception('Model windows by ratio failed.\n{}'.format(e))
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
else:
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@edit.command('windows-by-ratio-rect')
|
|
162
|
+
@click.argument('model-file', type=click.Path(
|
|
163
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
164
|
+
@click.argument('ratio', type=float)
|
|
165
|
+
@click.option('--aperture-height', '-ah', help='A number for the target height of the '
|
|
166
|
+
'output apertures. This can include the units of the distance (eg. 3ft) '
|
|
167
|
+
'or, if no units are provided the value will be interpreted in the '
|
|
168
|
+
'honeybee model units. Note that, if the ratio is too large for the '
|
|
169
|
+
'height, the ratio will take precedence and the actual aperture_height '
|
|
170
|
+
'will be larger than this value.',
|
|
171
|
+
type=str, default='2m', show_default=True)
|
|
172
|
+
@click.option('--sill-height', '-sh', help='A number for the target height above the '
|
|
173
|
+
'bottom edge of the rectangle to start the apertures. Note that, if the '
|
|
174
|
+
'ratio is too large for the height, the ratio will take precedence '
|
|
175
|
+
'and the sill_height will be smaller than this value. This can include '
|
|
176
|
+
'the units of the distance (eg. 3ft) or, if no units are provided, '
|
|
177
|
+
'the value will be interpreted in the honeybee model units.',
|
|
178
|
+
type=str, default='0.8m', show_default=True)
|
|
179
|
+
@click.option('--horizontal-separation', '-hs', help='A number for the target '
|
|
180
|
+
'separation between individual aperture center lines. If this number is '
|
|
181
|
+
'larger than the parent rectangle base, only one aperture will be '
|
|
182
|
+
'produced. This can include the units of the distance (eg. 3ft) or, if '
|
|
183
|
+
'no units are provided, the value will be interpreted in the honeybee '
|
|
184
|
+
'model units.', type=str, default='3m', show_default=True)
|
|
185
|
+
@click.option('--vertical-separation', '-vs', help='An optional number to create a '
|
|
186
|
+
'single vertical separation between top and bottom apertures. This can '
|
|
187
|
+
'include the units of the distance (eg. 3ft) or, if no units are provided '
|
|
188
|
+
'the value will be interpreted in the honeybee model units.',
|
|
189
|
+
type=str, default='0', show_default=True)
|
|
190
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
191
|
+
' with windows. By default it will be printed out to stdout',
|
|
192
|
+
type=click.File('w'), default='-')
|
|
193
|
+
def windows_by_ratio_rect(model_file, ratio, aperture_height, sill_height,
|
|
194
|
+
horizontal_separation, vertical_separation, output_file):
|
|
195
|
+
"""Add apertures to all outdoor walls of a model given a ratio.
|
|
196
|
+
|
|
197
|
+
Note that this method removes any existing apertures and doors from the Walls.
|
|
198
|
+
Any rectangular portions of walls will have customized rectangular apertures
|
|
199
|
+
using the various inputs.
|
|
200
|
+
|
|
201
|
+
\b
|
|
202
|
+
Args:
|
|
203
|
+
model_file: Full path to a Honeybee Model file.
|
|
204
|
+
ratio: A number between 0 and 1 (but not perfectly equal to 1)
|
|
205
|
+
for the desired ratio between window area and wall area.
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
# serialize the Model and check the Model tolerance
|
|
209
|
+
parsed_model = Model.from_file(model_file)
|
|
210
|
+
assert parsed_model.tolerance != 0, \
|
|
211
|
+
'Model must have a non-zero tolerance to use windows-by-ratio-rect.'
|
|
212
|
+
tol, units = parsed_model.tolerance, parsed_model.units
|
|
213
|
+
|
|
214
|
+
# convert distance strings to floats
|
|
215
|
+
aperture_height = parse_distance_string(aperture_height, units)
|
|
216
|
+
sill_height = parse_distance_string(sill_height, units)
|
|
217
|
+
horizontal_separation = parse_distance_string(horizontal_separation, units)
|
|
218
|
+
vertical_separation = parse_distance_string(vertical_separation, units)
|
|
219
|
+
|
|
220
|
+
# generate the windows for all walls of rooms
|
|
221
|
+
for room in parsed_model.rooms:
|
|
222
|
+
for face in room.faces:
|
|
223
|
+
if isinstance(face.boundary_condition, Outdoors) and \
|
|
224
|
+
isinstance(face.type, Wall):
|
|
225
|
+
face.apertures_by_ratio_rectangle(
|
|
226
|
+
ratio, aperture_height, sill_height, horizontal_separation,
|
|
227
|
+
vertical_separation, tol)
|
|
228
|
+
|
|
229
|
+
# write the new model out to the file or stdout
|
|
230
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
231
|
+
except Exception as e:
|
|
232
|
+
_logger.exception('Model windows by ratio rect failed.\n{}'.format(e))
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
else:
|
|
235
|
+
sys.exit(0)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@edit.command('extruded-border')
|
|
239
|
+
@click.argument('model-file', type=click.Path(
|
|
240
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
241
|
+
@click.option('--depth', '-d', help='A number for the extrusion depth. This can include '
|
|
242
|
+
'the units of the distance (eg. 3ft) or, if no units are provided, '
|
|
243
|
+
'the value will be interpreted in the honeybee model units.',
|
|
244
|
+
type=str, default='0.2m', show_default=True)
|
|
245
|
+
@click.option('--outdoor/--indoor', ' /-i', help='Flag to note whether the borders '
|
|
246
|
+
'should be on the indoors.', default=True, show_default=True)
|
|
247
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
248
|
+
' with borders. By default it will be printed out to stdout',
|
|
249
|
+
type=click.File('w'), default='-')
|
|
250
|
+
def extruded_border(model_file, depth, outdoor, output_file):
|
|
251
|
+
"""Add extruded borders to all windows in walls.
|
|
252
|
+
|
|
253
|
+
\b
|
|
254
|
+
Args:
|
|
255
|
+
model_file: Full path to a Honeybee Model file.
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
# serialize the Model to Python
|
|
259
|
+
parsed_model = Model.from_file(model_file)
|
|
260
|
+
indoor = not outdoor
|
|
261
|
+
|
|
262
|
+
# generate the overhangs for all walls of rooms
|
|
263
|
+
depth = parse_distance_string(depth, parsed_model.units)
|
|
264
|
+
for room in parsed_model.rooms:
|
|
265
|
+
for face in room.faces:
|
|
266
|
+
if isinstance(face.boundary_condition, Outdoors) and \
|
|
267
|
+
isinstance(face.type, Wall):
|
|
268
|
+
for ap in face.apertures:
|
|
269
|
+
ap.extruded_border(depth, indoor)
|
|
270
|
+
|
|
271
|
+
# write the new model out to the file or stdout
|
|
272
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
273
|
+
except Exception as e:
|
|
274
|
+
_logger.exception('Model extruded border failed.\n{}'.format(e))
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
else:
|
|
277
|
+
sys.exit(0)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@edit.command('overhang')
|
|
281
|
+
@click.argument('model-file', type=click.Path(
|
|
282
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
283
|
+
@click.option('--depth', '-d', help='A number for the overhang depth. This can include '
|
|
284
|
+
'the units of the distance (eg. 3ft) or, if no units are provided, '
|
|
285
|
+
'the value will be interpreted in the honeybee model units.',
|
|
286
|
+
type=str, default='1m', show_default=True)
|
|
287
|
+
@click.option('--angle', '-a', help='A number for the for an angle to rotate the '
|
|
288
|
+
'overhang in degrees. Positive numbers indicate a downward rotation while '
|
|
289
|
+
'negative numbers indicate an upward rotation.',
|
|
290
|
+
type=float, default=0, show_default=True)
|
|
291
|
+
@click.option('--vertical-offset', '-vo', help='An optional number for the vertical '
|
|
292
|
+
'offset of the overhang from the top of the window or face. Positive '
|
|
293
|
+
'numbers move up while negative mode down. This can include '
|
|
294
|
+
'the units of the distance (eg. 3ft) or, if no units are provided, '
|
|
295
|
+
'the value will be interpreted in the honeybee model units.',
|
|
296
|
+
type=str, default='0', show_default=True)
|
|
297
|
+
@click.option('--per-window/--per-wall', ' /-pw', help='Flag to note whether the '
|
|
298
|
+
'overhangs should be generated per aperture or per wall.',
|
|
299
|
+
default=True, show_default=True)
|
|
300
|
+
@click.option('--outdoor/--indoor', ' /-i', help='Flag to note whether the overhangs '
|
|
301
|
+
'should be on the indoors like a light shelf.',
|
|
302
|
+
default=True, show_default=True)
|
|
303
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
304
|
+
' with overhangs. By default it will be printed out to stdout',
|
|
305
|
+
type=click.File('w'), default='-')
|
|
306
|
+
def overhang(model_file, depth, angle, vertical_offset, per_window, outdoor,
|
|
307
|
+
output_file):
|
|
308
|
+
"""Add overhangs to all outdoor walls or windows in walls.
|
|
309
|
+
|
|
310
|
+
\b
|
|
311
|
+
Args:
|
|
312
|
+
model_file: Full path to a Honeybee Model file.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
# serialize the Model to Python and check the Model tolerance
|
|
316
|
+
parsed_model = Model.from_file(model_file)
|
|
317
|
+
assert parsed_model.tolerance != 0, \
|
|
318
|
+
'Model must have a non-zero tolerance to use overhang.'
|
|
319
|
+
tol, units = parsed_model.tolerance, parsed_model.units
|
|
320
|
+
indoor = not outdoor
|
|
321
|
+
|
|
322
|
+
# generate the overhangs for all walls of rooms
|
|
323
|
+
depth = parse_distance_string(depth, units)
|
|
324
|
+
overhangs = []
|
|
325
|
+
for room in parsed_model.rooms:
|
|
326
|
+
for face in room.faces:
|
|
327
|
+
if isinstance(face.boundary_condition, Outdoors) and \
|
|
328
|
+
isinstance(face.type, Wall):
|
|
329
|
+
if per_window:
|
|
330
|
+
for ap in face.apertures:
|
|
331
|
+
overhangs.extend(ap.overhang(depth, angle, indoor, tol))
|
|
332
|
+
else:
|
|
333
|
+
overhangs.extend(face.overhang(depth, angle, indoor, tol))
|
|
334
|
+
|
|
335
|
+
# move the overhangs if an offset has been specified
|
|
336
|
+
vertical_offset = parse_distance_string(vertical_offset, units)
|
|
337
|
+
if vertical_offset != 0:
|
|
338
|
+
m_vec = Vector3D(0, 0, vertical_offset)
|
|
339
|
+
for shd in overhangs:
|
|
340
|
+
shd.move(m_vec)
|
|
341
|
+
|
|
342
|
+
# write the new model out to the file or stdout
|
|
343
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
344
|
+
except Exception as e:
|
|
345
|
+
_logger.exception('Model overhang failed.\n{}'.format(e))
|
|
346
|
+
sys.exit(1)
|
|
347
|
+
else:
|
|
348
|
+
sys.exit(0)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@edit.command('louvers-by-count')
|
|
352
|
+
@click.argument('model-file', type=click.Path(
|
|
353
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
354
|
+
@click.argument('louver-count', type=int)
|
|
355
|
+
@click.option('--depth', '-d', help='A number for the depth of the louvers. This can '
|
|
356
|
+
'include the units of the distance (eg. 1ft) or, if no units are '
|
|
357
|
+
'provided, the value will be interpreted in the honeybee model units.',
|
|
358
|
+
type=str, default='0.25m', show_default=True)
|
|
359
|
+
@click.option('--angle', '-a', help='A number for the for an angle to rotate the '
|
|
360
|
+
'louvers in degrees. Positive numbers indicate a downward rotation while '
|
|
361
|
+
'negative numbers indicate an upward rotation.',
|
|
362
|
+
type=float, default=0, show_default=True)
|
|
363
|
+
@click.option('--offset', '-o', help='An optional number for the offset of the louvers '
|
|
364
|
+
'from base Face or Aperture. This can include the units of the distance '
|
|
365
|
+
'(eg. 1ft) or, if no units are provided, the value will be interpreted in '
|
|
366
|
+
'the honeybee model units.', type=str, default='0', show_default=True)
|
|
367
|
+
@click.option('--horizontal/--vertical', ' /-v', help='Flag to note whether louvers '
|
|
368
|
+
'are horizontal or vertical.', default=True, show_default=True)
|
|
369
|
+
@click.option('--per-window/--per-wall', ' /-pw', help='Flag to note whether the '
|
|
370
|
+
'louvers should be generated per aperture or per wall.',
|
|
371
|
+
default=True, show_default=True)
|
|
372
|
+
@click.option('--outdoor/--indoor', ' /-i', help='Flag to note whether the louvers '
|
|
373
|
+
'should be on the indoors like a light shelf.',
|
|
374
|
+
default=True, show_default=True)
|
|
375
|
+
@click.option('--no-flip/--flip-start', ' /-fs', help='Flag to note whether the '
|
|
376
|
+
'the side that the louvers start from should be flipped. If not flipped, '
|
|
377
|
+
'louvers will start from top or right. If flipped, they will start from '
|
|
378
|
+
'the bottom or left.', default=True, show_default=True)
|
|
379
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
380
|
+
' with louvers. By default it will be printed out to stdout',
|
|
381
|
+
type=click.File('w'), default='-')
|
|
382
|
+
def louvers_by_count(model_file, louver_count, depth, angle, offset, horizontal,
|
|
383
|
+
per_window, outdoor, no_flip, output_file):
|
|
384
|
+
"""Add louvers to all outdoor walls or windows in walls.
|
|
385
|
+
|
|
386
|
+
\b
|
|
387
|
+
Args:
|
|
388
|
+
model_file: Full path to a Honeybee Model file.
|
|
389
|
+
louver_count: A positive integer for the number of louvers to generate.
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
# serialize the Model and check the Model tolerance
|
|
393
|
+
parsed_model = Model.from_file(model_file)
|
|
394
|
+
assert parsed_model.tolerance != 0, \
|
|
395
|
+
'Model must have a non-zero tolerance to use overhang.'
|
|
396
|
+
tol, units = parsed_model.tolerance, parsed_model.units
|
|
397
|
+
indoor = not outdoor
|
|
398
|
+
flip_start = not no_flip
|
|
399
|
+
cont_vec = Vector2D(0, 1) if horizontal else Vector2D(1, 0)
|
|
400
|
+
|
|
401
|
+
# generate the overhangs for all walls of rooms
|
|
402
|
+
depth = parse_distance_string(depth, units)
|
|
403
|
+
offset = parse_distance_string(offset, units)
|
|
404
|
+
for room in parsed_model.rooms:
|
|
405
|
+
for face in room.faces:
|
|
406
|
+
if isinstance(face.boundary_condition, Outdoors) and \
|
|
407
|
+
isinstance(face.type, Wall):
|
|
408
|
+
if per_window:
|
|
409
|
+
for ap in face.apertures:
|
|
410
|
+
ap.louvers_by_count(louver_count, depth, offset, angle,
|
|
411
|
+
cont_vec, flip_start, indoor, tol)
|
|
412
|
+
else:
|
|
413
|
+
face.louvers_by_count(louver_count, depth, offset, angle,
|
|
414
|
+
cont_vec, flip_start, indoor, tol)
|
|
415
|
+
|
|
416
|
+
# write the new model out to the file or stdout
|
|
417
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
418
|
+
except Exception as e:
|
|
419
|
+
_logger.exception('Model louver generation failed.\n{}'.format(e))
|
|
420
|
+
sys.exit(1)
|
|
421
|
+
else:
|
|
422
|
+
sys.exit(0)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@edit.command('louvers-by-spacing')
|
|
426
|
+
@click.argument('model-file', type=click.Path(
|
|
427
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
428
|
+
@click.option('--spacing', '-s', help='A number for the distance between each louver. '
|
|
429
|
+
'This can include the units of the distance (eg. 2ft) or, if no units are '
|
|
430
|
+
'provided, the value will be interpreted in the honeybee model units.',
|
|
431
|
+
type=str, default='0.5m', show_default=True)
|
|
432
|
+
@click.option('--depth', '-d', help='A number for the depth of the louvers. This can '
|
|
433
|
+
'include the units of the distance (eg. 1ft) or, if no units are '
|
|
434
|
+
'provided, the value will be interpreted in the honeybee model units.',
|
|
435
|
+
type=str, default='0.25m', show_default=True)
|
|
436
|
+
@click.option('--angle', '-a', help='A number for the for an angle to rotate the '
|
|
437
|
+
'louvers in degrees. Positive numbers indicate a downward rotation while '
|
|
438
|
+
'negative numbers indicate an upward rotation.',
|
|
439
|
+
type=float, default=0, show_default=True)
|
|
440
|
+
@click.option('--offset', '-o', help='An optional number for the offset of the louvers '
|
|
441
|
+
'from base Face or Aperture. This can include the units of the distance '
|
|
442
|
+
'(eg. 1ft) or, if no units are provided, the value will be interpreted in '
|
|
443
|
+
'the honeybee model units.', type=str, default='0', show_default=True)
|
|
444
|
+
@click.option('--horizontal/--vertical', ' /-v', help='Flag to note wh.',
|
|
445
|
+
default=True, show_default=True)
|
|
446
|
+
@click.option('--max-count', '-m', help='Optional integer to set the maximum number of '
|
|
447
|
+
'louvers that will be generated. If 0, louvers will cover the entire '
|
|
448
|
+
'face.', type=int, default=0, show_default=True)
|
|
449
|
+
@click.option('--per-window/--per-wall', ' /-pw', help='Flag to note whether the '
|
|
450
|
+
'louvers should be generated per aperture or per wall.',
|
|
451
|
+
default=True, show_default=True)
|
|
452
|
+
@click.option('--outdoor/--indoor', ' /-i', help='Flag to note whether the louvers '
|
|
453
|
+
'should be on the indoors like a light shelf.',
|
|
454
|
+
default=True, show_default=True)
|
|
455
|
+
@click.option('--no-flip/--flip-start', ' /-fs', help='Flag to note whether the '
|
|
456
|
+
'the side that the louvers start from should be flipped. If not flipped, '
|
|
457
|
+
'louvers will start from top or right. If flipped, they will start from '
|
|
458
|
+
'the bottom or left.', default=True, show_default=True)
|
|
459
|
+
@click.option('--output-file', '-f', help='Optional file to output the Model JSON string'
|
|
460
|
+
' with louvers. By default it will be printed out to stdout',
|
|
461
|
+
type=click.File('w'), default='-')
|
|
462
|
+
def louvers_by_spacing(model_file, spacing, depth, angle, offset, horizontal,
|
|
463
|
+
max_count, per_window, outdoor, no_flip, output_file):
|
|
464
|
+
"""Add louvers to all outdoor walls or windows in walls.
|
|
465
|
+
|
|
466
|
+
\b
|
|
467
|
+
Args:
|
|
468
|
+
model_file: Full path to a Honeybee Model file.
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
# serialize the Model to Python and check the Model tolerance
|
|
472
|
+
parsed_model = Model.from_file(model_file)
|
|
473
|
+
assert parsed_model.tolerance != 0, \
|
|
474
|
+
'Model must have a non-zero tolerance to use overhang.'
|
|
475
|
+
tol, units = parsed_model.tolerance, parsed_model.units
|
|
476
|
+
indoor = not outdoor
|
|
477
|
+
flip_start = not no_flip
|
|
478
|
+
cont_vec = Vector2D(0, 1) if horizontal else Vector2D(1, 0)
|
|
479
|
+
|
|
480
|
+
# generate the overhangs for all walls of rooms
|
|
481
|
+
spacing = parse_distance_string(spacing, units)
|
|
482
|
+
depth = parse_distance_string(depth, units)
|
|
483
|
+
offset = parse_distance_string(offset, units)
|
|
484
|
+
for room in parsed_model.rooms:
|
|
485
|
+
for face in room.faces:
|
|
486
|
+
if isinstance(face.boundary_condition, Outdoors) and \
|
|
487
|
+
isinstance(face.type, Wall):
|
|
488
|
+
if per_window:
|
|
489
|
+
for ap in face.apertures:
|
|
490
|
+
ap.louvers_by_distance_between(
|
|
491
|
+
spacing, depth, offset, angle, cont_vec,
|
|
492
|
+
flip_start, indoor, tol, max_count)
|
|
493
|
+
else:
|
|
494
|
+
face.louvers_by_distance_between(
|
|
495
|
+
spacing, depth, offset, angle, cont_vec, flip_start,
|
|
496
|
+
indoor, tol, max_count)
|
|
497
|
+
|
|
498
|
+
# write the new model out to the file or stdout
|
|
499
|
+
output_file.write(json.dumps(parsed_model.to_dict()))
|
|
500
|
+
except Exception as e:
|
|
501
|
+
_logger.exception('Model louver generation failed.\n{}'.format(e))
|
|
502
|
+
sys.exit(1)
|
|
503
|
+
else:
|
|
504
|
+
sys.exit(0)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@edit.command('reset-resource-ids')
|
|
508
|
+
@click.argument('model-file', type=click.Path(
|
|
509
|
+
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
|
|
510
|
+
@click.option(
|
|
511
|
+
'--by-name/--by-name-and-uuid', ' /-uuid', help='Flag to note whether '
|
|
512
|
+
'newly-generated resource object IDs should be derived only from a '
|
|
513
|
+
'cleaned display_name or whether this new ID should also have a unique '
|
|
514
|
+
'set of 8 characters appended to it to guarantee uniqueness.', default=True
|
|
515
|
+
)
|
|
516
|
+
@click.option(
|
|
517
|
+
'--output-file', '-f', help='Optional hbjson file to output the JSON '
|
|
518
|
+
'string of the converted model. By default this will be printed out to '
|
|
519
|
+
'stdout', type=click.File('w'), default='-', show_default=True
|
|
520
|
+
)
|
|
521
|
+
def reset_resource_ids(model_file, by_name, output_file):
|
|
522
|
+
"""Reset the identifiers of all resource objects in a Model file.
|
|
523
|
+
|
|
524
|
+
This will reset the identifiers of all resources of all extensions and
|
|
525
|
+
is useful when human-readable names are needed when the model is
|
|
526
|
+
exported to simulation engines.
|
|
527
|
+
|
|
528
|
+
\b
|
|
529
|
+
Args:
|
|
530
|
+
model_file: Full path to a Honeybee Model (HBJSON) file.
|
|
531
|
+
"""
|
|
532
|
+
try:
|
|
533
|
+
# load the model file and separately load up the resource objects
|
|
534
|
+
if sys.version_info < (3, 0):
|
|
535
|
+
with open(model_file) as inf:
|
|
536
|
+
data = json.load(inf)
|
|
537
|
+
else:
|
|
538
|
+
with open(model_file, encoding='utf-8') as inf:
|
|
539
|
+
data = json.load(inf)
|
|
540
|
+
model = Model.from_dict(data)
|
|
541
|
+
# reset the identifiers of resources in the dictionary
|
|
542
|
+
add_uuid = not by_name
|
|
543
|
+
for atr in model.properties._extension_attributes:
|
|
544
|
+
var = getattr(model.properties, atr)
|
|
545
|
+
if not hasattr(var, 'reset_resource_ids_in_dict'):
|
|
546
|
+
continue
|
|
547
|
+
try:
|
|
548
|
+
data = var.reset_resource_ids_in_dict(data, add_uuid)
|
|
549
|
+
except Exception as e:
|
|
550
|
+
import traceback
|
|
551
|
+
traceback.print_exc()
|
|
552
|
+
raise Exception('Failed to reset resource IDs for {}: {}'.format(var, e))
|
|
553
|
+
# write the dictionary into a JSON
|
|
554
|
+
output_file.write(json.dumps(data))
|
|
555
|
+
except Exception as e:
|
|
556
|
+
_logger.exception('Resetting resource identifiers failed.\n{}'.format(e))
|
|
557
|
+
sys.exit(1)
|
|
558
|
+
else:
|
|
559
|
+
sys.exit(0)
|
honeybee/cli/lib.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
import zipfile
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from honeybee.config import folders
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(help='Commands for managing the standards library.')
|
|
14
|
+
def lib():
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@lib.command('purge')
|
|
19
|
+
@click.option(
|
|
20
|
+
'--standards-folder', '-s', default=None, help='A directory containing sub-folders '
|
|
21
|
+
'of resource objects to be purged of files. If unspecified, the default user '
|
|
22
|
+
'standards folder will be used.', type=click.Path(
|
|
23
|
+
exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
'--backup/--no-backup', ' /-xb', help='Flag to note whether a backup .zip file '
|
|
27
|
+
'of the user standards library should be made before the purging operation. '
|
|
28
|
+
'This is done by default in case the user ever wants to recover their old '
|
|
29
|
+
'standards but can be turned off if a backup is not desired.',
|
|
30
|
+
default=True, show_default=True
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
'--log-file', '-log', help='Optional file to output a log of the purging process. '
|
|
34
|
+
'By default this will be printed out to stdout',
|
|
35
|
+
type=click.File('w'), default='-', show_default=True
|
|
36
|
+
)
|
|
37
|
+
def purge_lib(standards_folder, backup, log_file):
|
|
38
|
+
"""Purge the library of all user standards that it contains.
|
|
39
|
+
|
|
40
|
+
This is useful when a user's standard library has become filled with duplicated
|
|
41
|
+
objects or the user wishes to start fresh by re-exporting updated objects.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
# set the folder to the default standards_folder if unspecified
|
|
45
|
+
folder = standards_folder if standards_folder is not None else \
|
|
46
|
+
folders.default_standards_folder
|
|
47
|
+
if folder is None:
|
|
48
|
+
msg = 'No standards folder could be found. Nothing was purged.'
|
|
49
|
+
log_file.write(msg)
|
|
50
|
+
else:
|
|
51
|
+
resources = [std for std in os.listdir(folder)
|
|
52
|
+
if os.path.isdir(os.path.join(folder, std))]
|
|
53
|
+
sub_folders = [os.path.join(folder, std) for std in resources]
|
|
54
|
+
|
|
55
|
+
# make a backup of the folder if requested
|
|
56
|
+
if backup:
|
|
57
|
+
r_names, s_files, s_paths = [], [], []
|
|
58
|
+
for sf, r_name in zip(sub_folders, resources):
|
|
59
|
+
for s_file in os.listdir(sf):
|
|
60
|
+
s_path = os.path.join(sf, s_file)
|
|
61
|
+
if os.path.isfile(s_path):
|
|
62
|
+
r_names.append(r_name)
|
|
63
|
+
s_files.append(s_file)
|
|
64
|
+
s_paths.append(s_path)
|
|
65
|
+
if len(s_paths) != 0: # there are resources to back up
|
|
66
|
+
backup_name = '.standards_backup_{}.zip'.format(
|
|
67
|
+
str(datetime.now()).split('.')[0].replace(':', '-'))
|
|
68
|
+
backup_file = os.path.join(os.path.dirname(folder), backup_name)
|
|
69
|
+
with zipfile.ZipFile(backup_file, 'w') as zf:
|
|
70
|
+
for r_name, s_file, s_path in zip(r_names, s_files, s_paths):
|
|
71
|
+
zf.write(s_path, os.path.join(r_name, s_file))
|
|
72
|
+
|
|
73
|
+
# loop through the sub-folders and delete the files
|
|
74
|
+
rel_files = []
|
|
75
|
+
for sf in sub_folders:
|
|
76
|
+
for s_file in os.listdir(sf):
|
|
77
|
+
s_path = os.path.join(sf, s_file)
|
|
78
|
+
if os.path.isfile(s_path):
|
|
79
|
+
rel_files.append(s_path)
|
|
80
|
+
purged_files, fail_files = [], []
|
|
81
|
+
for rf in rel_files:
|
|
82
|
+
try:
|
|
83
|
+
os.remove(rf)
|
|
84
|
+
purged_files.append(rf)
|
|
85
|
+
except Exception:
|
|
86
|
+
fail_files.append(rf)
|
|
87
|
+
|
|
88
|
+
# report all of the deleted files in the log file
|
|
89
|
+
if len(rel_files) == 0:
|
|
90
|
+
log_file.write('The standards folder is empty so no files were removed.')
|
|
91
|
+
if len(purged_files) != 0:
|
|
92
|
+
msg = 'The following files were removed in the purging ' \
|
|
93
|
+
'operations:\n{}\n'.format(' \n'.join(purged_files))
|
|
94
|
+
log_file.write(msg)
|
|
95
|
+
if len(fail_files) != 0:
|
|
96
|
+
msg = 'The following files could not be removed in the purging ' \
|
|
97
|
+
'operations:\n{}\n'.format(' \n'.join(fail_files))
|
|
98
|
+
log_file.write(msg)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
_logger.exception('Purging user standards library failed.\n{}'.format(e))
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
else:
|
|
103
|
+
sys.exit(0)
|