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/typing.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""Collection of methods for type input checking."""
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
import math
|
|
5
|
+
import uuid
|
|
6
|
+
import hashlib
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
INFPOS = math.inf
|
|
10
|
+
INFNEG = -1 * math.inf
|
|
11
|
+
except AttributeError:
|
|
12
|
+
# python 2
|
|
13
|
+
INFPOS = float('inf')
|
|
14
|
+
INFNEG = float('-inf')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def valid_string(value, input_name=''):
|
|
18
|
+
"""Check that a string is valid for both Radiance and EnergyPlus.
|
|
19
|
+
|
|
20
|
+
This is used for honeybee geometry object names.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
illegal_match = re.search(r'[^.A-Za-z0-9_-]', value)
|
|
24
|
+
except TypeError:
|
|
25
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
26
|
+
input_name, type(value), value))
|
|
27
|
+
assert illegal_match is None, 'Illegal character "{}" found in {}'.format(
|
|
28
|
+
illegal_match.group(0), input_name)
|
|
29
|
+
assert len(value) > 0, 'Input {} "{}" contains no characters.'.format(
|
|
30
|
+
input_name, value)
|
|
31
|
+
assert len(value) <= 100, 'Input {} "{}" must be less than 100 characters.'.format(
|
|
32
|
+
input_name, value)
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def valid_rad_string(value, input_name=''):
|
|
37
|
+
"""Check that a string is valid for Radiance.
|
|
38
|
+
|
|
39
|
+
This is used for radiance modifier names, etc.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
illegal_match = re.search(r'[^.A-Za-z0-9_-]', value)
|
|
43
|
+
except TypeError:
|
|
44
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
45
|
+
input_name, type(value), value))
|
|
46
|
+
assert illegal_match is None, 'Illegal character "{}" found in {}'.format(
|
|
47
|
+
illegal_match.group(0), input_name)
|
|
48
|
+
assert len(value) > 0, 'Input {} "{}" contains no characters.'.format(
|
|
49
|
+
input_name, value)
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def valid_ep_string(value, input_name=''):
|
|
54
|
+
"""Check that a string is valid for EnergyPlus.
|
|
55
|
+
|
|
56
|
+
This is used for energy material names, schedule names, etc.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
non_ascii = tuple(i for i in value if ord(i) >= 128)
|
|
60
|
+
except TypeError:
|
|
61
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
62
|
+
input_name, type(value), value))
|
|
63
|
+
assert non_ascii == (), 'Illegal characters {} found in {}'.format(
|
|
64
|
+
non_ascii, input_name)
|
|
65
|
+
illegal_match = re.search(r'[,;!\n\t]', value)
|
|
66
|
+
assert illegal_match is None, 'Illegal character "{}" found in {}'.format(
|
|
67
|
+
illegal_match.group(0), input_name)
|
|
68
|
+
assert len(value) > 0, 'Input {} "{}" contains no characters.'.format(
|
|
69
|
+
input_name, value)
|
|
70
|
+
assert len(value) <= 100, 'Input {} "{}" must be less than 100 characters.'.format(
|
|
71
|
+
input_name, value)
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _number_check(value, input_name):
|
|
76
|
+
"""Check if value is a number."""
|
|
77
|
+
try:
|
|
78
|
+
number = float(value)
|
|
79
|
+
except (ValueError, TypeError):
|
|
80
|
+
raise TypeError('Input {} must be a number. Got {}: {}.'.format(
|
|
81
|
+
input_name, type(value), value))
|
|
82
|
+
return number
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def float_in_range(value, mi=INFNEG, ma=INFPOS, input_name=''):
|
|
86
|
+
"""Check a float value to be between minimum and maximum."""
|
|
87
|
+
number = _number_check(value, input_name)
|
|
88
|
+
assert mi <= number <= ma, 'Input number {} must be between {} and {}. ' \
|
|
89
|
+
'Got {}'.format(input_name, mi, ma, value)
|
|
90
|
+
return number
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def float_in_range_excl(value, mi=INFNEG, ma=INFPOS, input_name=''):
|
|
94
|
+
"""Check a float value to be greater than minimum and less than maximum."""
|
|
95
|
+
number = _number_check(value, input_name)
|
|
96
|
+
assert mi < number < ma, 'Input number {} must be greater than {} ' \
|
|
97
|
+
'and less than {}. Got {}'.format(input_name, mi, ma, value)
|
|
98
|
+
return number
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def float_in_range_excl_incl(value, mi=INFNEG, ma=INFPOS, input_name=''):
|
|
102
|
+
"""Check a float value to be greater than minimum and less than/equal to maximum."""
|
|
103
|
+
number = _number_check(value, input_name)
|
|
104
|
+
assert mi < number <= ma, 'Input number {} must be greater than {} and less than ' \
|
|
105
|
+
'or equal to {}. Got {}'.format(input_name, mi, ma, value)
|
|
106
|
+
return number
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def float_in_range_incl_excl(value, mi=INFNEG, ma=INFPOS, input_name=''):
|
|
110
|
+
"""Check a float value to be greater than/equal to minimum and less than maximum."""
|
|
111
|
+
number = _number_check(value, input_name)
|
|
112
|
+
assert mi <= number < ma, 'Input number {} must be greater than or equal to {} ' \
|
|
113
|
+
'and less than {}. Got {}'.format(input_name, mi, ma, value)
|
|
114
|
+
return number
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def int_in_range(value, mi=INFNEG, ma=INFPOS, input_name=''):
|
|
118
|
+
"""Check an integer value to be between minimum and maximum."""
|
|
119
|
+
try:
|
|
120
|
+
number = int(value)
|
|
121
|
+
except ValueError:
|
|
122
|
+
# try to convert to float and then digit if possible
|
|
123
|
+
try:
|
|
124
|
+
number = int(float(value))
|
|
125
|
+
except (ValueError, TypeError):
|
|
126
|
+
raise TypeError('Input {} must be an integer. Got {}: {}.'.format(
|
|
127
|
+
input_name, type(value), value))
|
|
128
|
+
except (ValueError, TypeError):
|
|
129
|
+
raise TypeError('Input {} must be an integer. Got {}: {}.'.format(
|
|
130
|
+
input_name, type(value), value))
|
|
131
|
+
assert mi <= number <= ma, 'Input integer {} must be between {} and {}. ' \
|
|
132
|
+
'Got {}.'.format(input_name, mi, ma, value)
|
|
133
|
+
return number
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def float_positive(value, input_name=''):
|
|
137
|
+
"""Check a float value to be positive."""
|
|
138
|
+
return float_in_range(value, 0, INFPOS, input_name)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def int_positive(value, input_name=''):
|
|
142
|
+
"""Check if an integer value is positive."""
|
|
143
|
+
return int_in_range(value, 0, INFPOS, input_name)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def tuple_with_length(value, length=3, item_type=float, input_name=''):
|
|
147
|
+
"""Try to create a tuple with a certain value."""
|
|
148
|
+
try:
|
|
149
|
+
value = tuple(item_type(v) for v in value)
|
|
150
|
+
except (ValueError, TypeError):
|
|
151
|
+
raise TypeError('Input {} must be a {}.'.format(
|
|
152
|
+
input_name, item_type))
|
|
153
|
+
assert len(value) == length, 'Input {} length must be {} not {}'.format(
|
|
154
|
+
input_name, length, len(value))
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def list_with_length(value, length=3, item_type=float, input_name=''):
|
|
159
|
+
"""Try to create a list with a certain value."""
|
|
160
|
+
try:
|
|
161
|
+
value = [item_type(v) for v in value]
|
|
162
|
+
except (ValueError, TypeError):
|
|
163
|
+
raise TypeError('Input {} must be a {}.'.format(
|
|
164
|
+
input_name, item_type))
|
|
165
|
+
assert len(value) == length, 'Input {} length must be {} not {}'.format(
|
|
166
|
+
input_name, length, len(value))
|
|
167
|
+
return value
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def clean_string(value, input_name=''):
|
|
171
|
+
"""Clean a string so that it is valid for both Radiance and EnergyPlus.
|
|
172
|
+
|
|
173
|
+
This will strip out spaces and special characters and raise an error if the
|
|
174
|
+
string is has more than 100 characters. If the input has no valid characters
|
|
175
|
+
after stripping out illegal ones, a randomly-generated UUID will be returned.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
value = value.replace(' ', '_') # spaces > underscores for readability
|
|
179
|
+
val = re.sub(r'[^.A-Za-z0-9_-]', '', value)
|
|
180
|
+
except TypeError:
|
|
181
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
182
|
+
input_name, type(value), value))
|
|
183
|
+
if len(val) == 0: # generate a unique but consistent ID from the input
|
|
184
|
+
sha256_hash = hashlib.sha256(value.encode('utf-8'))
|
|
185
|
+
hash_str = str(sha256_hash.hexdigest())
|
|
186
|
+
return hash_str[:8] if len(hash_str) > 8 else hash_str
|
|
187
|
+
if len(val) > 100:
|
|
188
|
+
val = val[:100]
|
|
189
|
+
return val
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def clean_rad_string(value, input_name=''):
|
|
193
|
+
"""Clean a string for Radiance that can be used for rad material names.
|
|
194
|
+
|
|
195
|
+
This includes stripping out illegal characters and white spaces. If the input
|
|
196
|
+
has no valid characters after stripping out illegal ones, a randomly-generated
|
|
197
|
+
UUID will be returned.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
value = value.replace(' ', '_') # spaces > underscores for readability
|
|
201
|
+
val = re.sub(r'[^.A-Za-z0-9_-]', '', value)
|
|
202
|
+
except TypeError:
|
|
203
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
204
|
+
input_name, type(value), value))
|
|
205
|
+
if len(val) == 0: # generate a unique but consistent ID from the input
|
|
206
|
+
sha256_hash = hashlib.sha256(value.encode('utf-8'))
|
|
207
|
+
hash_str = str(sha256_hash.hexdigest())
|
|
208
|
+
return hash_str[:8] if len(hash_str) > 8 else hash_str
|
|
209
|
+
return val
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def clean_ep_string(value, input_name=''):
|
|
213
|
+
"""Clean a string for EnergyPlus that can be used for energy material names.
|
|
214
|
+
|
|
215
|
+
This includes stripping out all illegal characters, removing trailing spaces,
|
|
216
|
+
and rasing an error if the name is not longer than 100 characters. If the input
|
|
217
|
+
has no valid characters after stripping out illegal ones, a randomly-generated
|
|
218
|
+
UUID will be returned.
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
val = ''.join(i for i in value if ord(i) < 128) # strip out non-ascii
|
|
222
|
+
val = re.sub(r'[,;!\n\t]', '', val) # strip out E+ special characters
|
|
223
|
+
except TypeError:
|
|
224
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
225
|
+
input_name, type(value), value))
|
|
226
|
+
val = val.strip()
|
|
227
|
+
if len(val) == 0: # generate a unique but consistent ID from the input
|
|
228
|
+
sha256_hash = hashlib.sha256(value.encode('utf-8'))
|
|
229
|
+
hash_str = str(sha256_hash.hexdigest())
|
|
230
|
+
return hash_str[:8] if len(hash_str) > 8 else hash_str
|
|
231
|
+
if len(val) > 100:
|
|
232
|
+
val = val[:100]
|
|
233
|
+
return val
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def clean_and_id_string(value, input_name=''):
|
|
237
|
+
"""Clean a string and add 8 unique characters to it to make it unique.
|
|
238
|
+
|
|
239
|
+
Strings longer than 50 characters will be truncated before adding the ID.
|
|
240
|
+
The resulting string will be valid for both Radiance and EnergyPlus.
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
value = value.replace(' ', '_') # spaces > underscores for readability
|
|
244
|
+
val = re.sub(r'[^.A-Za-z0-9_-]', '', value)
|
|
245
|
+
except TypeError:
|
|
246
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
247
|
+
input_name, type(value), value))
|
|
248
|
+
if len(val) > 50:
|
|
249
|
+
val = val[:50]
|
|
250
|
+
return val + '_' + str(uuid.uuid4())[:8]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def clean_and_id_rad_string(value, input_name=''):
|
|
254
|
+
"""Clean a string and add 8 unique characters to it to make it unique for Radiance.
|
|
255
|
+
|
|
256
|
+
This includes stripping out illegal characters and white spaces.
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
value = value.replace(' ', '_') # spaces > underscores for readability
|
|
260
|
+
val = re.sub(r'[^.A-Za-z0-9_-]', '', value)
|
|
261
|
+
except TypeError:
|
|
262
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
263
|
+
input_name, type(value), value))
|
|
264
|
+
return val + '_' + str(uuid.uuid4())[:8]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def clean_and_id_ep_string(value, input_name=''):
|
|
268
|
+
"""Clean a string and add 8 unique characters to it to make it unique for EnergyPlus.
|
|
269
|
+
|
|
270
|
+
This includes stripping out all illegal characters and removing trailing white spaces.
|
|
271
|
+
Strings longer than 50 characters will be truncated before adding the ID.
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
val = ''.join(i for i in value if ord(i) < 128) # strip out non-ascii
|
|
275
|
+
val = re.sub(r'[,;!\n\t]', '', val) # strip out E+ special characters
|
|
276
|
+
except TypeError:
|
|
277
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
278
|
+
input_name, type(value), value))
|
|
279
|
+
val = val.strip()
|
|
280
|
+
if len(val) > 50:
|
|
281
|
+
val = val[:50]
|
|
282
|
+
return val + '_' + str(uuid.uuid4())[:8]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def clean_and_number_string(value, existing_dict, input_name=''):
|
|
286
|
+
"""Clean a string and add an integer to it if it is found in the existing_dict.
|
|
287
|
+
|
|
288
|
+
The resulting string will be valid for both Radiance and EnergyPlus.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
value: The text string to be cleaned and possibly given a unique integer.
|
|
292
|
+
existing_dict: A dictionary where the keys are text strings of existing items
|
|
293
|
+
and the values are the number of times that the item has appeared in
|
|
294
|
+
the model already.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
value = value.replace(' ', '_') # spaces > underscores for readability
|
|
298
|
+
val = re.sub(r'[^.A-Za-z0-9_-]', '_', value)
|
|
299
|
+
except TypeError:
|
|
300
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
301
|
+
input_name, type(value), value))
|
|
302
|
+
if len(val) > 95:
|
|
303
|
+
val = val[:95]
|
|
304
|
+
if val in existing_dict:
|
|
305
|
+
existing_dict[val] += 1
|
|
306
|
+
return val + '_' + str(existing_dict[val])
|
|
307
|
+
else:
|
|
308
|
+
existing_dict[val] = 1
|
|
309
|
+
return val
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def clean_and_number_rad_string(value, existing_dict, input_name=''):
|
|
313
|
+
"""Clean a string for Radiance and add an integer if found in the existing_dict.
|
|
314
|
+
|
|
315
|
+
This includes stripping out illegal characters and white spaces.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
value: The text string to be cleaned and possibly given a unique integer.
|
|
319
|
+
existing_dict: A dictionary where the keys are text strings of existing items
|
|
320
|
+
and the values are the number of times that the item has appeared in
|
|
321
|
+
the model already.
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
value = value.replace(' ', '_') # spaces > underscores for readability
|
|
325
|
+
val = re.sub(r'[^.A-Za-z0-9_-]', '_', value)
|
|
326
|
+
except TypeError:
|
|
327
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
328
|
+
input_name, type(value), value))
|
|
329
|
+
if val in existing_dict:
|
|
330
|
+
existing_dict[val] += 1
|
|
331
|
+
return val + '_' + str(existing_dict[val])
|
|
332
|
+
else:
|
|
333
|
+
existing_dict[val] = 1
|
|
334
|
+
return val
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def clean_and_number_ep_string(value, existing_dict, input_name=''):
|
|
338
|
+
"""Clean a string for EnergyPlus and add an integer if found in the existing_dict.
|
|
339
|
+
|
|
340
|
+
This includes stripping out all illegal characters and removing trailing white spaces.
|
|
341
|
+
Strings longer than 95 characters will be truncated before adding the integer.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
value: The text string to be cleaned and possibly given a unique integer.
|
|
345
|
+
existing_dict: A dictionary where the keys are text strings of existing items
|
|
346
|
+
and the values are the number of times that the item has appeared in
|
|
347
|
+
the model already.
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
val = ''.join(i for i in value if ord(i) < 128) # strip out non-ascii
|
|
351
|
+
val = re.sub(r'[,;!\n\t]', '', val) # strip out E+ special characters
|
|
352
|
+
except TypeError:
|
|
353
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
354
|
+
input_name, type(value), value))
|
|
355
|
+
val = val.strip()
|
|
356
|
+
if len(val) > 95:
|
|
357
|
+
val = val[:95]
|
|
358
|
+
if val in existing_dict:
|
|
359
|
+
existing_dict[val] += 1
|
|
360
|
+
return val + ' ' + str(existing_dict[val])
|
|
361
|
+
else:
|
|
362
|
+
existing_dict[val] = 1
|
|
363
|
+
return val
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def truncate_and_id_string(value, truncate_len=32, uuid_len=0, input_name=''):
|
|
367
|
+
"""Truncate a string to a length with an option to add unique characters at the end.
|
|
368
|
+
|
|
369
|
+
Note that all outputs will always be the truncate_len or less and the uuid_len
|
|
370
|
+
just specifies the number of characters to replace at the end with unique ones.
|
|
371
|
+
|
|
372
|
+
The result will be valid for EnergyPlus, Radiance, and likely many more engines
|
|
373
|
+
with different types of character restrictions.
|
|
374
|
+
"""
|
|
375
|
+
try:
|
|
376
|
+
value = value.replace(' ', '_') # spaces > underscores for readability
|
|
377
|
+
val = re.sub(r'[^.A-Za-z0-9_-]', '', value)
|
|
378
|
+
except TypeError:
|
|
379
|
+
raise TypeError('Input {} must be a text string. Got {}: {}.'.format(
|
|
380
|
+
input_name, type(value), value))
|
|
381
|
+
final_len = truncate_len - uuid_len
|
|
382
|
+
if len(val) > final_len:
|
|
383
|
+
val = val[:final_len]
|
|
384
|
+
if uuid_len > 0:
|
|
385
|
+
return val + str(uuid.uuid4())[:uuid_len]
|
|
386
|
+
return val
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def fixed_string_length(value, target_len=32):
|
|
390
|
+
"""Truncate a string or add trailing spaces to hit a target character length.
|
|
391
|
+
|
|
392
|
+
This is useful when trying to construct human-readable tables of text.
|
|
393
|
+
"""
|
|
394
|
+
if len(value) > target_len:
|
|
395
|
+
return value[:target_len]
|
|
396
|
+
elif len(value) < target_len:
|
|
397
|
+
return value + ' ' * (target_len - len(value))
|
|
398
|
+
else:
|
|
399
|
+
return value
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def readable_short_name(value, max_length=24):
|
|
403
|
+
"""Convert a string into a shorter but readable version of itself.
|
|
404
|
+
|
|
405
|
+
This is useful when dealing with interfaces of file formats that have very
|
|
406
|
+
strict character limits on names or identifiers (like in DOE-2/eQuest).
|
|
407
|
+
|
|
408
|
+
When ths input is less than or equal to the max length, the string will be
|
|
409
|
+
left as-is. If not, then the lower-case vowels will be removed from the name,
|
|
410
|
+
making the result abbreviated but still readable/recognizable. If the result
|
|
411
|
+
is still not shorter than the max length, then spaces will be removed. Lastly,
|
|
412
|
+
if all else fails to meet the max length, the middle characters will be,
|
|
413
|
+
removed leaving the beginning and end as they are, which should typically
|
|
414
|
+
help preserve the uniqueness of the name.
|
|
415
|
+
|
|
416
|
+
Note that this method does not do any check for illegal characters and presumes
|
|
417
|
+
that the input is already composed of legal characters.
|
|
418
|
+
"""
|
|
419
|
+
# perform an initial check to see if it passes
|
|
420
|
+
if len(value) <= max_length:
|
|
421
|
+
return value
|
|
422
|
+
# strip out lowercase vowels and special characters like dashes
|
|
423
|
+
try:
|
|
424
|
+
value = re.sub(r'[aeiouy_\-]', '', value)
|
|
425
|
+
except TypeError:
|
|
426
|
+
raise TypeError('Input must be a text string. Got {}: {}.'.format(
|
|
427
|
+
type(value), value))
|
|
428
|
+
if len(value) <= max_length:
|
|
429
|
+
return value
|
|
430
|
+
# remove spaces from the string to see if it gets short enough
|
|
431
|
+
value = value.replace(' ', '')
|
|
432
|
+
if len(value) <= max_length:
|
|
433
|
+
return value
|
|
434
|
+
# lastly, remove some characters from the middle to get it to fit
|
|
435
|
+
mid_ind = int(max_length * 0.5)
|
|
436
|
+
assert mid_ind > 3, \
|
|
437
|
+
'Max character length of {} is too restrictive.'.format(max_length)
|
|
438
|
+
end_length = max_length - mid_ind - 1
|
|
439
|
+
value = '{}_{}'.format(value[:mid_ind], value[-end_length:])
|
|
440
|
+
return value
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def clean_doe2_string(value, max_length=24):
|
|
444
|
+
"""Clean and shorten a string for DOE-2 so that it can be a U-name.
|
|
445
|
+
|
|
446
|
+
This includes stripping out all illegal characters (including both non-ASCII
|
|
447
|
+
and DOE-2 specific characters), removing trailing spaces, and passing the
|
|
448
|
+
result through the readable_short_name function to hit the target max_length.
|
|
449
|
+
Note that white spaces can be in the result with the assumption that
|
|
450
|
+
the name will be enclosed in double quotes.
|
|
451
|
+
|
|
452
|
+
Note that this method does not do any check to ensure the string is unique.
|
|
453
|
+
"""
|
|
454
|
+
try:
|
|
455
|
+
val = ''.join(i for i in value if ord(i) < 128) # strip out non-ascii
|
|
456
|
+
val = re.sub(r'["\(\)\[\]\,\=\n\t]', '', val) # remove DOE-2 special characters
|
|
457
|
+
val = val.replace('_', ' ') # put back white spaces
|
|
458
|
+
except TypeError:
|
|
459
|
+
raise TypeError('Input must be a text string. Got {}: {}.'.format(
|
|
460
|
+
type(value), value))
|
|
461
|
+
val = val.strip()
|
|
462
|
+
if len(val) == 0: # generate a unique but consistent ID from the input
|
|
463
|
+
sha256_hash = hashlib.sha256(value.encode('utf-8'))
|
|
464
|
+
hash_str = str(sha256_hash.hexdigest())
|
|
465
|
+
return hash_str[:8] if len(hash_str) > 8 else hash_str
|
|
466
|
+
return readable_short_name(val, max_length)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def invalid_dict_error(invalid_dict, error):
|
|
470
|
+
"""Raise a ValueError for an invalid dictionary that failed to serialize.
|
|
471
|
+
|
|
472
|
+
This error message will include the identifier (and display_name) if they are
|
|
473
|
+
present within the invalid_dict, making it easier for ens users to find the
|
|
474
|
+
invalid object within large objects like Models.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
invalid_dict: A dictionary of an invalid honeybee object that failed
|
|
478
|
+
to serialize.
|
|
479
|
+
error:
|
|
480
|
+
"""
|
|
481
|
+
obj_type = invalid_dict['type'].replace('Abridged', '') \
|
|
482
|
+
if 'type' in invalid_dict else 'Honeybee Object'
|
|
483
|
+
obj_id = invalid_dict['identifier'] if 'identifier' in invalid_dict else ''
|
|
484
|
+
full_id = '{}[{}]'.format(invalid_dict['display_name'], obj_id) \
|
|
485
|
+
if 'display_name' in invalid_dict else obj_id
|
|
486
|
+
raise ValueError('{} "{}" is invalid:\n{}'.format(obj_type, full_id, error))
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
wrapper = '"' if os.name == 'nt' else '\''
|
|
490
|
+
"""String wrapper."""
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def normpath(value):
|
|
494
|
+
"""Normalize path eliminating double slashes, etc and put it in quotes if needed."""
|
|
495
|
+
value = os.path.normpath(value)
|
|
496
|
+
if ' ' in value:
|
|
497
|
+
value = '{0}{1}{0}'.format(wrapper, value)
|
|
498
|
+
return value
|
honeybee/units.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Utility functions for converting and parsing units of length."""
|
|
2
|
+
|
|
3
|
+
# global properties to set all supported units
|
|
4
|
+
UNITS = ('Meters', 'Millimeters', 'Feet', 'Inches', 'Centimeters')
|
|
5
|
+
UNITS_ABBREVIATIONS = ('m', 'mm', 'ft', 'in', 'cm')
|
|
6
|
+
UNITS_TOLERANCES = {
|
|
7
|
+
'Meters': 0.01,
|
|
8
|
+
'Millimeters': 1.0,
|
|
9
|
+
'Feet': 0.01,
|
|
10
|
+
'Inches': 0.1,
|
|
11
|
+
'Centimeters': 1.0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def conversion_factor_to_meters(units):
|
|
16
|
+
"""Get the conversion factor to meters based on input units.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
units: Text for the units. Choose from the following:
|
|
20
|
+
|
|
21
|
+
* Meters
|
|
22
|
+
* Millimeters
|
|
23
|
+
* Feet
|
|
24
|
+
* Inches
|
|
25
|
+
* Centimeters
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A number for the conversion factor, which should be multiplied by
|
|
29
|
+
all distance units taken from Rhino geometry in order to convert
|
|
30
|
+
them to meters.
|
|
31
|
+
"""
|
|
32
|
+
if units == 'Meters':
|
|
33
|
+
return 1.0
|
|
34
|
+
elif units == 'Millimeters':
|
|
35
|
+
return 0.001
|
|
36
|
+
elif units == 'Feet':
|
|
37
|
+
return 0.3048
|
|
38
|
+
elif units == 'Inches':
|
|
39
|
+
return 0.0254
|
|
40
|
+
elif units == 'Centimeters':
|
|
41
|
+
return 0.01
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
'You are kidding me! What units are you using? {}?\n'
|
|
45
|
+
'Please use one of the following: {}'.format(units, ' '.join(UNITS))
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_distance_string(distance_string, destination_units='Meters'):
|
|
50
|
+
"""Parse a string of a distance value into a destination units system.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
distance_string: Text for a distance value to be parsed into the
|
|
54
|
+
destination units. This can have the units at the end of
|
|
55
|
+
it (eg. "3ft"). If no units are included, the number will be
|
|
56
|
+
assumed to be in the destination units system.
|
|
57
|
+
destination_units: The destination units system to which the distance
|
|
58
|
+
string will be computed. (Default: Meters).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
A number for the distance in the destination_units.
|
|
62
|
+
"""
|
|
63
|
+
# separate the distance string into a number and a unit abbreviation
|
|
64
|
+
distance_string = distance_string.strip().replace(',', '.')
|
|
65
|
+
try: # check if the distance string is just a number
|
|
66
|
+
return float(distance_string)
|
|
67
|
+
except ValueError: # it must have some units attached to it
|
|
68
|
+
for i, ua in enumerate(UNITS_ABBREVIATIONS):
|
|
69
|
+
try: # see if replacing the units yields a float
|
|
70
|
+
distance = float(distance_string.replace(ua, '', 1))
|
|
71
|
+
u_sys = UNITS[i]
|
|
72
|
+
break
|
|
73
|
+
except ValueError: # not the right type of units
|
|
74
|
+
pass
|
|
75
|
+
else: # we could not match the units system
|
|
76
|
+
raise ValueError(
|
|
77
|
+
'Text string "{}" could not be decoded into a distance and a unit.\n'
|
|
78
|
+
'Make sure your units are one of the following: {}'.format(
|
|
79
|
+
distance_string, ' '.join(UNITS_ABBREVIATIONS))
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# process the number into the destination units system
|
|
83
|
+
if u_sys == destination_units:
|
|
84
|
+
return distance
|
|
85
|
+
con_factor = 1 / conversion_factor_to_meters(destination_units)
|
|
86
|
+
if u_sys != 'Meters':
|
|
87
|
+
con_factor = con_factor * conversion_factor_to_meters(u_sys)
|
|
88
|
+
return distance * con_factor
|
honeybee/writer/door.py
ADDED
honeybee/writer/face.py
ADDED
honeybee/writer/model.py
ADDED
honeybee/writer/room.py
ADDED
honeybee/writer/shade.py
ADDED