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.
Files changed (48) hide show
  1. honeybee/__init__.py +23 -0
  2. honeybee/__main__.py +4 -0
  3. honeybee/_base.py +331 -0
  4. honeybee/_basewithshade.py +310 -0
  5. honeybee/_lockable.py +99 -0
  6. honeybee/altnumber.py +47 -0
  7. honeybee/aperture.py +997 -0
  8. honeybee/boundarycondition.py +358 -0
  9. honeybee/checkdup.py +173 -0
  10. honeybee/cli/__init__.py +118 -0
  11. honeybee/cli/compare.py +132 -0
  12. honeybee/cli/create.py +265 -0
  13. honeybee/cli/edit.py +559 -0
  14. honeybee/cli/lib.py +103 -0
  15. honeybee/cli/setconfig.py +43 -0
  16. honeybee/cli/validate.py +224 -0
  17. honeybee/colorobj.py +363 -0
  18. honeybee/config.json +5 -0
  19. honeybee/config.py +347 -0
  20. honeybee/dictutil.py +54 -0
  21. honeybee/door.py +746 -0
  22. honeybee/extensionutil.py +208 -0
  23. honeybee/face.py +2360 -0
  24. honeybee/facetype.py +153 -0
  25. honeybee/logutil.py +79 -0
  26. honeybee/model.py +4272 -0
  27. honeybee/orientation.py +132 -0
  28. honeybee/properties.py +845 -0
  29. honeybee/room.py +3485 -0
  30. honeybee/search.py +107 -0
  31. honeybee/shade.py +514 -0
  32. honeybee/shademesh.py +362 -0
  33. honeybee/typing.py +498 -0
  34. honeybee/units.py +88 -0
  35. honeybee/writer/__init__.py +7 -0
  36. honeybee/writer/aperture.py +6 -0
  37. honeybee/writer/door.py +6 -0
  38. honeybee/writer/face.py +6 -0
  39. honeybee/writer/model.py +6 -0
  40. honeybee/writer/room.py +6 -0
  41. honeybee/writer/shade.py +6 -0
  42. honeybee/writer/shademesh.py +6 -0
  43. honeybee_core-1.64.12.dist-info/METADATA +94 -0
  44. honeybee_core-1.64.12.dist-info/RECORD +48 -0
  45. honeybee_core-1.64.12.dist-info/WHEEL +5 -0
  46. honeybee_core-1.64.12.dist-info/entry_points.txt +2 -0
  47. honeybee_core-1.64.12.dist-info/licenses/LICENSE +661 -0
  48. 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)