-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsimstocktwo.py
1609 lines (1443 loc) · 76.1 KB
/
simstocktwo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
/***************************************************************************
SimstockQGIS
copyright : (C) 2023 by UCL
email : [email protected]
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
import os
import math
import platform
import pandas as pd
from ast import literal_eval
from shapely.wkt import loads
from eppy.modeleditor import IDF, IDDAlreadySetError
from time import time, localtime, strftime
from shapely.geometry import LineString, MultiLineString
from shapely.ops import unary_union
import json
from eppy.bunch_subclass import BadEPFieldError
import logging
ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
EP_DIR = os.path.join(ROOT_DIR, 'EnergyPlus')
#IDF_DIR = os.path.join(ROOT_DIR, 'idf_files')
#os.makedirs(IDF_DIR, exist_ok=True)
ep_basic_settings = os.path.join(ROOT_DIR, 'basic_settings.idf')
input_data = os.path.join(ROOT_DIR, 'sa_preprocessed.csv')
# Do not place window if the wall width is less than this number
min_avail_width_for_window = 1
# Do not place window if partially exposed external wall is less than this number % of zone height
min_avail_height = 80
def main(idf_dir):
# User set cwd from main script
IDF_DIR = idf_dir
os.makedirs(IDF_DIR, exist_ok=True)
start = time()
print('__________________________________________________________________',
flush=True)
print(strftime('%d/%m/%Y %H:%M:%S', localtime()),
'- {} start time'.format(os.path.basename(__file__)), flush=True)
logging.info("Simstock idf geometry started")
# Find the computer's operating system and set path to E+ idd file
system = platform.system().lower()
if system in ['windows', 'linux', 'darwin']:
iddfile = os.path.join(EP_DIR, 'ep8.9_{}/Energy+.idd'.format(system))
try:
IDF.setiddname(iddfile) #not needed due to prior setup fns setting idd
except IDDAlreadySetError:
pass
idf = IDF(ep_basic_settings)
# Load input data (preprocessing outputs)
df = pd.read_csv(input_data, dtype={'construction':str})
# Load config file
with open(os.path.join(ROOT_DIR, "config.json"), "r") as read_file:
config = json.load(read_file)
# Function which creates the idf(s)
def createidfs(bi_df, df, mode):
# Move all objects towards origins
origin = loads(bi_df['sa_polygon'].iloc[0])
origin = list(origin.exterior.coords[0])
origin.append(0)
# Shading volumes converted to shading objects
shading_df = bi_df.loc[bi_df['shading'] == True]
shading_df.apply(shading_volumes, args=(df, idf, origin,), axis=1)
# Polygons with zones converted to thermal zones based on floor number
zones_df = bi_df.loc[bi_df['shading'] == False]
zone_use_dict = {} #mixed-use for plugin ###############################
zones_df.apply(thermal_zones, args=(bi_df, idf, origin, zone_use_dict,), axis=1)
# Extract names of thermal zones:
zones = idf.idfobjects['ZONE']
zone_names = list()
for zone in zones:
zone_names.append(zone.Name)
# Create a 'Dwell' zone list with all thermal zones. "Dwell" apears
# in all objects which reffer to all zones (thermostat, people, etc.)
#idf.newidfobject('ZONELIST', Name='List')
#objects = idf.idfobjects['ZONELIST'][-1]
#for i, zone in enumerate(zone_names):
# exec('objects.Zone_%s_Name = zone' % (i + 1))
########################################################################
# Plugin feature: mixed-use
mixed_use(idf, zone_use_dict)
########################################################################
# Ideal loads system
for zone in zone_names:
system_name = '{}_HVAC'.format(zone)
eq_name = '{}_Eq'.format(zone)
supp_air_node = '{}_supply'.format(zone)
air_node = '{}_air_node'.format(zone)
ret_air_node = '{}_return'.format(zone)
idf.newidfobject('ZONEHVAC:IDEALLOADSAIRSYSTEM',
Name=system_name,
Zone_Supply_Air_Node_Name=supp_air_node,
Dehumidification_Control_Type='None')
idf.newidfobject('ZONEHVAC:EQUIPMENTLIST',
Name=eq_name,
Zone_Equipment_1_Object_Type='ZONEHVAC:IDEALLOADSAIRSYSTEM',
Zone_Equipment_1_Name=system_name,
Zone_Equipment_1_Cooling_Sequence=1,
Zone_Equipment_1_Heating_or_NoLoad_Sequence=1)
idf.newidfobject('ZONEHVAC:EQUIPMENTCONNECTIONS',
Zone_Name=zone,
Zone_Conditioning_Equipment_List_Name=eq_name,
Zone_Air_Inlet_Node_or_NodeList_Name=supp_air_node,
Zone_Air_Node_Name=air_node,
Zone_Return_Air_Node_or_NodeList_Name=ret_air_node)
####################################################################
# Plugin feature: fields sourced from attribute table
# TODO: streamline this by putting in functions
def get_osgb_value(val_name, zones_df, zone):
"""Gets the value of a specified attribute for the zone"""
osgb_from_zone = "_".join(zone.split("_")[:-2])
return zones_df[zones_df["osgb"]==osgb_from_zone][val_name].to_numpy()[0]
# Get specified inputs for zone
ventilation_rate = get_osgb_value("ventilation_rate", zones_df, zone)
infiltration_rate = get_osgb_value("infiltration_rate", zones_df, zone)
# Get the rest of the default obj values from dict
zone_ventilation_dict = ventilation_dict
zone_infiltration_dict = infiltration_dict
# Set the name, zone name and ventilation rate
zone_ventilation_dict["Name"] = zone + "_ventilation"
zone_ventilation_dict["Zone_or_ZoneList_Name"] = zone
zone_ventilation_dict["Air_Changes_per_Hour"] = ventilation_rate
zone_ventilation_dict["Schedule_Name"] = zone_use_dict[zone] + "_Occ"
zone_ventilation_dict["Minimum_Indoor_Temperature"] = float(config["Ventilation minimum temperature"])
# Same for infiltration
zone_infiltration_dict["Name"] = zone + "_infiltration"
zone_infiltration_dict["Zone_or_ZoneList_Name"] = zone
zone_infiltration_dict["Air_Changes_per_Hour"] = infiltration_rate
# Add the ventilation idf object
idf.newidfobject(**zone_ventilation_dict)
idf.newidfobject(**zone_infiltration_dict)
####################################################################
#if mode == "single":
# idf.saveas(os.path.join(IDF_DIR, '{}.idf'.format(datafilename)))
#elif mode == "bi":
# idf.saveas(os.path.join(BI_IDF_DIR, '{}.idf'.format(bi)))
idf.saveas(os.path.join(IDF_DIR, '{}.idf'.format(bi)))
# Check whether in built island mode and create idf(s) accordingly
#if args.builtisland:
#print("Splitting output data into built islands")
#os.makedirs(BI_IDF_DIR, exist_ok=True)
bi_list = df['bi'].unique().tolist()
for bi in bi_list:
idf = IDF(ep_basic_settings)
# Change the name field of the building object
building_object = idf.idfobjects['BUILDING'][0]
building_object.Name = bi
# Get the data for the BI
bi_df = df[df['bi'] == bi]
# Get the data for other BIs to use as shading
rest = df[df['bi'] != bi]
# Shading buffer with specified radius
buffer_radius = float(config["Shading buffer radius - m"])
# Buffer the BI geometry to specified radius
bi_geom = list(map(loads, bi_df.polygon.to_numpy()))
buffer = unary_union(bi_geom).convex_hull.buffer(buffer_radius)
# Find polygons which are within this buffer and create mask
lst = []
index = []
for row in rest.itertuples():
poly = loads(row.sa_polygon)
# The following is True if poly intersects buffer and False if not
lst.append(poly.intersects(buffer))
index.append(row.Index)
mask = pd.Series(lst, index=index)
# Get data for the polygons within the buffer
within_buffer = rest.loc[mask].copy()
# Set them to be shading
within_buffer["shading"] = True
# Include them in the idf for the BI
bi_df = pd.concat([bi_df, within_buffer])
# Only create idf if the BI is not entirely composed of shading blocks
shading_vals = bi_df['shading'].to_numpy()
if not shading_vals.all():
createidfs(bi_df, df, "bi")
else:
continue
#else:
# # Change the name field of the building object
# building_object = idf.idfobjects['BUILDING'][0]
# building_object.Name = datafilename
# createidfs(df, "single")
pt('##### idf_geometry created in:', start)
# END OF MAIN - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def mixed_use(idf, zone_use_dict):
# Check for missing values
for key, value in zone_use_dict.items():
if not isinstance(value, str) and math.isnan(value):
raise ValueError("{} has no value for 'use'.".format(key))
# Create a zonelist for each use
use_list = list(zone_use_dict.values())
use_list = list(map(str.lower, use_list)) #remove case-sensitivity
use_list = list(set(use_list))
for use in use_list:
zone_list = list()
for key, value in zone_use_dict.items():
if value.lower() == use:
zone_list.append(key)
idf.newidfobject('ZONELIST', Name=use)
objects = idf.idfobjects['ZONELIST'][-1]
for i, zone in enumerate(zone_list):
exec('objects.Zone_%s_Name = zone' % (i + 1))
objects_to_delete = list()
for obj in ['PEOPLE', 'LIGHTS', 'ELECTRICEQUIPMENT',
'ZONEINFILTRATION:DESIGNFLOWRATE',
'ZONECONTROL:THERMOSTAT']:
objects = idf.idfobjects[obj]
for item in objects:
try:
if item.Zone_or_ZoneList_Name.lower() not in use_list:
objects_to_delete.append(item)
# Newer versions of E+ use this alternate attribute for some but not all objects
except BadEPFieldError:
if item.Zone_or_ZoneList_or_Space_or_SpaceList_Name.lower() not in use_list:
objects_to_delete.append(item)
for item in objects_to_delete:
idf.removeidfobject(item)
def pt(printout, pst):
pft = time()
process_time = pft - pst
if process_time <= 60:
unit = 'sec'
elif process_time <= 3600:
process_time = process_time / 60
unit = 'min'
else:
process_time = process_time / 3600
unit = 'hr'
loctime = strftime('%d/%m/%Y %H:%M:%S', localtime())
print('{0} - {1} {2:.2f} {3}'.format(loctime, printout, process_time, unit), flush=True)
return
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def shading_volumes(row, df, idf, origin):
'''
Function which generates idf geometry for surrounding Build Blocks. All
elements are converted to shading objects
'''
# Polygon name and coordinates
osgb, polygon = row.osgb, loads(row.sa_polygon)
# Polygon with removed collinear point to be used for ceiling/floor/roof
hor_polygon = row.sa_polygon_horizontal
# Convert polygon coordinates to dictionary of outer and inner
# (if any) coordinates
hor_poly_coord_dict = polygon_coordinates_dictionary(hor_polygon)
# List of adjacent polygons
adj_osgb_list = literal_eval(str(row.sa_collinear_touching))
# Load the polygon which defines only external surfaces
ext_surf_polygon = loads(row.sa_polygon_exposed_wall)
# List of external surface only coordinates (ext_surf_polygon +
# inner rings)
ext_surf_coord = surface_coordinates(ext_surf_polygon, origin)
# # List of horizontal surfaces coordinates (roof/floor/ceiling)
horiz_surf_coord = horizontal_surface_coordinates(
hor_poly_coord_dict, origin)
# Zone bottom/top vertical vertex
zone_floor_h = 0
zone_ceiling_h = row.height
# Include adiabatic roof
adiabatic_roof(idf, osgb, horiz_surf_coord, zone_ceiling_h)
adiabatic_wall_name = 'AdiabaticWall'
# Create external walls
adiabatic_external_walls(idf, osgb, ext_surf_coord, zone_ceiling_h,
zone_floor_h, adiabatic_wall_name,
adj_osgb_list, df, polygon, origin)
return
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def polygon_coordinates_dictionary(polygon):
'''
Function which stores data form POLYGON((,,,),(,,,),(,,,)) in dictionary.
Data are in the shape Polygon(exterior[, interiors=None])
'''
# Load the polygon data by using shapely
polygon = loads(polygon)
# Empty dictionary
polygon_coordinates_dict = dict()
# Outer ring (exterior) coordinates
polygon_coordinates_dict['outer_ring'] = [polygon.exterior.coords]
# If there are inner rings (holes in polygon) than loop through then and
# store them in the list
if polygon.interiors:
polygon_coordinates_dict['inner_rings'] = list() # empty list
for item in polygon.interiors:
polygon_coordinates_dict['inner_rings'].append(item.coords)
# Return the dictionary
return polygon_coordinates_dict
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def surface_coordinates(polygon, origin):
'''
Function which creates a list of coordinates lists depending on the polygon
type.
'''
# Empty coordinates list
coordinates_list = list()
# If polygon geometry type is 'MultiLineString' than loop through it and
# extract coordinates for each 'LineString'. If polygon type is
# GeometryCollection than it is composed of 'Point' and 'LineString' (it
# can also be 'MultiLineString' but not as part of this module processing).
# Exclude 'Point' coordinates. When polygon geometry type is 'LinearRing'
# or 'LineString' than take their coordinates and append the coordinates
# list
if polygon.geom_type in ['MultiLineString', 'GeometryCollection']:
for item in polygon.geoms:
if not item.geom_type == 'Point':
coordinates_list.append(item.coords)
elif polygon.geom_type == 'LineString':
coordinates_list.append(polygon.coords)
# Position coordinates relative to origin
coordinates = coordinates_move_origin(coordinates_list, origin)
# Return coordinates
return coordinates
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def coordinates_move_origin(coordinates_list, origin):
'''
Function which positions a coordinates relative to the origin
'''
coordinates_list_moved_origin = list()
# Loop through the list of coordinates
for coordinates in coordinates_list:
# Empty list which to hold coordinates
coordinates_moved_origin = list()
# Loop through coordinates
for ordered_pair in coordinates:
# Position ordered pairs relative to origin
ordered_pair_moved_origin = [i - j for i, j in zip(ordered_pair,
origin)]
# Round ordered pairs to 2 decimal spaces
ordered_pair_moved_origin = [
round(coord, 2) for coord in ordered_pair_moved_origin]
coordinates_moved_origin.append(ordered_pair_moved_origin)
coordinates_list_moved_origin.append(coordinates_moved_origin)
# Return list of coordinates converted to text strings
return coordinates_list_moved_origin
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def horizontal_surface_coordinates(coordinates_dictionary, origin):
'''
Function which adjust coordinates to be suitable for creating E+ horizontal
surfaces. If polygon has no holes than it just converts polygon coordinates
to a list of text strings positioned relative to the origin. Coordinates
for polygon with holes are slightly more complex since hole areas has to be
subtracted from polygon area by following counter-clock rule.
'''
if len(coordinates_dictionary) == 1:
coordinates_list = coordinates_dictionary['outer_ring']
else:
coordinates_list = polygon_with_holes(coordinates_dictionary)
# Position coordinates relative to the origin
coordinates = coordinates_move_origin(coordinates_list, origin)
# Return coordinates converted to text
return coordinates
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def polygon_with_holes(coordinates_dictionary):
'''
Function which merges outer and inned rings in suitable format (coordinate
pairs order) so EnergyPlus can recognise it as a surface with holes. The
logic is to find the minimum distance between outer ring coordinates and
inner ring coordinates which are connected with the LineString. There might
be more than oneLineString connecting outer and inner ring if there is more
than one hole. The LineString is used to split outer and inner LineStrings
in MultiLineStrings (in case that the closest coordinate is not the first
coordinate of outer/inner LineString). Outer and inner LineStrings are
reorganised in suitable order. For example, first LineString from outer
MultiLineString is extended with the LineString connecting outer and inner
rings than inner ring LineString are added in CCW order followed by the
same LineString connecting inner and outer rings. The last outer LineString
is added to close the ring
'''
def polygon_with_holes_coordinates(coordinates, outer_ring_linestring,
interceptors_op_dict):
'''
Internal function which links inner ring coordinates to outer ring
connection point
'''
# Get the first outer ring LineStings's ordered pair
first_op = outer_ring_linestring.coords[0]
# Check whether the first outer ring LineStings's ordered pair is the
# point from which the inner ring is connected to the outer ring. It
# won't be only for the first LineString of the outer ring
# MultiLineString when the hole is not connected to the outer ring in
# the first ordered pair
if first_op in interceptors_op_dict:
# Append the list with the first outer ring LineStings's ordered
# pair
coordinates.append(first_op)
# Extract from the dictionary the inner ring LineString /
# MultiLineString which is connected to the outer ring in the
# first_op. It can be more than one inner ring connecting to this
# ordered pair
holes = interceptors_op_dict[first_op]
# Loop through the list of inner rings
for hole in holes:
# When the inner ring is LineString it means that the inner
# ring is connected to the outer ring via the first ordered
# pair of inner ring. In that case append coordinates of the
# inner ring to the first ordered pair of outer ring
if hole.geom_type == 'LineString':
for coord in hole.coords:
coordinates.append(coord)
# In case of MultiLineString than first append the second
# LineString of inner ring followed by the first LineString
# (excluding the first ordered pair of the first LineString
# which is the same as the last ordered pair of the second
# LineString)
if hole.geom_type == 'MultiLineString':
for coord in hole.geoms[1].coords:
coordinates.append(coord)
for coord in hole.geoms[0].coords[1:]:
coordinates.append(coord)
# Close the coordinates by adding the first outer ring
# LineStings's ordered pair
coordinates.append(first_op)
# Add the rest of outer ring LineString coordinates to the
# coordinate list
for coord in outer_ring_linestring.coords[1:-1]:
coordinates.append(coord)
# If the first outer ring LineStings's ordered pair is not linked to
# the inner ring that append coordinates with outer ring LineString
# (excluding the last ordered pair which is actually the link to the
# inner ring but it is covered in the next outer ring LineString)
else:
for coord in outer_ring_linestring.coords[:-1]:
coordinates.append(coord)
# Return coordinates of outer ring LineString with inner ring
return coordinates
def dist_two_points(p1, p2):
'''
Internal function which calculates the Euclidean distance between two
points
'''
distance = math.sqrt(math.pow(
(p1[0] - p2[0]), 2) + math.pow((p1[1] - p2[1]), 2))
# Return distance between two points
return distance
def inner_string(inner_ring_coordinates, irop):
inner_coordinates = list(inner_ring_coordinates)
for i, coord in enumerate(inner_coordinates):
if coord == irop:
split_position = i
break
if split_position == 0:
inner_linestring = LineString(inner_coordinates)
else:
first = inner_coordinates[:(split_position + 1)]
last = inner_coordinates[split_position:]
inner_linestring = MultiLineString([first, last])
return inner_linestring
# Coordinates of the outer ring extracted from the coordinates dictionary
outer_ring_coordinates = coordinates_dictionary['outer_ring']
# Interceptors ordered pairs dictionary
interceptors_op_dict = {}
# List of LineStings connecting outer ring and inner ring coordinates
oi_min_linestring_list = []
# Loop through the list of inner rings (holes)
for inner_ring_coordinates in coordinates_dictionary['inner_rings']:
# orop / irop: outer / inner ring ordered pair
# Loop through the coordinates of outer ring
for i, orop in enumerate(outer_ring_coordinates[0][:-1]):
# Loop through coordinates of inner ring
for j, irop in enumerate(inner_ring_coordinates[:-1]):
# Set an initial minimum distance between outer and inner rings
# for first ordered pairs of outer and inner rings
if i == 0 and j == 0:
# outer ring - inner ring coordinates minimum distance
oi_min_distance = dist_two_points(orop, irop)
if oi_min_distance > 0.015:
# outer ring - inner ring minimum distance LineString
oi_min_linestring = LineString([orop, irop])
# Get the difference between inner ring and
# intersection point between inner ring and LineSting
# connecting inner ring and outer ring. If intersection
# point is not the first coordinate of inner ring than
# inner ring will be broken to a MultiLineString
inner_linestring = inner_string(inner_ring_coordinates,
irop)
else:
oi_min_distance = 1e9
# Update the minimum distance between outer and inner rings,
# LineString connecting these outer and inner ring along the
# minimum distance and inner ring LineSting (MultiLineSting) by
# checking the distance between other inner and outer
# coordinates
else:
distance = dist_two_points(orop, irop)
if (distance < oi_min_distance) and (distance > 0.015):
oi_min_distance = distance
oi_min_linestring = LineString([orop, irop])
inner_linestring = inner_string(inner_ring_coordinates,
irop)
# Append the dictionary with the key: ordered pair at outer ring /
# value: list of inner rings LineSting or MultiLineString
if oi_min_linestring.coords[0] in interceptors_op_dict:
interceptors_op_dict[oi_min_linestring.coords[0]].append(
inner_linestring)
else:
interceptors_op_dict[
oi_min_linestring.coords[0]] = [inner_linestring]
# Populate the list of LineStrings connecting outer and inner rings
oi_min_linestring_list.append(oi_min_linestring)
# Intersect outer ring with LineStrings connecting outer ring with inner
# rings in order to identify points where inner holes are connected to
# outer ring
outer_ring = MultiLineString(coordinates_dictionary['outer_ring'])
for oi_min_linestring in oi_min_linestring_list:
outer_ring = outer_ring.difference(oi_min_linestring)
# Empty list to hold arranged coordinates
coordinates = []
# When outer ring is LineString it means that there is only one hole and it
# is connected to outer ring from the first ordered pair of outer ring
# coordinate list
if outer_ring.geom_type == 'LineString':
# First and last coordinate of polygon with holes coordinates is equal
# to the first ordered pair of outer ring. It is appended to the end of
# the coordinates list
start_end_op = outer_ring.coords[0]
# Get the polygon with holes coordinates
coordinates = polygon_with_holes_coordinates(coordinates, outer_ring,
interceptors_op_dict)
# When outer ring is MultiLineString it means that it can be (1) only one
# hole which is connected to outer ring at other than first ordered pair of
# outer ring coordinate list, (2) more than one hole: (a) where one hole
# can be connected to outer ring at the first ordered pair of outer ring
# coordinate list or (b) no holes are connected to outer ring at the first
# ordered pair of outer ring coordinate list
elif outer_ring.geom_type == 'MultiLineString':
# Loop through LineString in a MultiLineString
for i, linestring in enumerate(outer_ring.geoms):
# First and last ordered pair of polygon with holes coordinates is
# the first coordinate of the first LineString
if i == 0:
start_end_op = linestring.coords[0]
# Get the polygon with holes coordinates
coordinates = polygon_with_holes_coordinates(coordinates,
linestring,
interceptors_op_dict)
# Append the polygon with holes coordinates with start-end ordered pair
coordinates.append(start_end_op)
# Return the list of polygon with holes coordinates
return [coordinates]
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def adiabatic_roof(idf, polygon_name, horizontal_surface_coordinates,
ceiling_height):
ceiling_coordinates_list = coordinates_add_height(
ceiling_height, horizontal_surface_coordinates)
coordinates_list = idf_ceiling_coordinates_list(ceiling_coordinates_list)
surface_name = polygon_name + '_AdiabaticRoof'
for coordinates in coordinates_list:
shading_building_detailed(idf, surface_name, coordinates)
return
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def coordinates_add_height(height, coordinates_list):
'''
Function which adds Z coordinate to coordinate pairs
'''
coordinates_with_height = []
# Loop through the coordinates in the list of coordinates
for coordinates in coordinates_list:
# Empty list for ordered pairs with height
ordered_pair_with_height = []
# Loop through coordinates and append each ordered pair with height
# rounded to 2 decimal spaces
for op in coordinates:
op_with_height = op + [round(height, 2)]
ordered_pair_with_height.append(op_with_height)
# Append coordinates with height list with ordered pair with height lst
coordinates_with_height.append(ordered_pair_with_height)
# Return ordered pair with height list
return coordinates_with_height
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def idf_ceiling_coordinates_list(ceiling_coordinates_list):
'''
Function which converts ceiling coordinates list into format used by E+
'''
idf_ceiling_coordinates_list = []
# Loop through a list of ceiling coordinates lists
for ceiling_coordinates in ceiling_coordinates_list:
# For each coordinates list remove the last element (which is same as
# first since energyplus surfaces don't end in the first coordinate)
ceiling_coordinates = ceiling_coordinates[:-1]
# Reverse the list of coordinates in order to have energyplus surface
# facing outside
ceiling_coordinates = list(reversed(ceiling_coordinates))
# Append a list of ceiling coordinates lists
idf_ceiling_coordinates_list.append(ceiling_coordinates)
# Return a list of ceiling coordinates lists
return idf_ceiling_coordinates_list
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def shading_building_detailed(idf, surface_name, coordinates):
'''
Function which creates ShadingBuilding:Detailed energyplus object
'''
idf.newidfobject(
'Shading:Building:Detailed'.upper(),
Name=surface_name)
# Add coordinates to the latest added energyplus object
objects = idf.idfobjects['Shading:Building:Detailed'.upper()][-1]
# Loop through coordinates list and assign X, Y, and Z vertex of each#
# ordered pair to the associated Vertex coordinate
for i, ordered_pair in enumerate(coordinates):
exec('objects.Vertex_{}_Xcoordinate = ordered_pair[0]'.format(i + 1))
exec('objects.Vertex_{}_Ycoordinate = ordered_pair[1]'.format(i + 1))
exec('objects.Vertex_{}_Zcoordinate = ordered_pair[2]'.format(i + 1))
return
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def adiabatic_external_walls(idf, polygon_name, perimeter_surface_coordinates,
ceiling_height, floor_height, wall_name,
adjacent_polygons_list, df,
polygon_shapely, origin):
'''
Function which generates energyplus object for adiabatic external walls. It
is composed of two parts (1) external walls which are not parts of adjacent
surfaces and (2) external walls which are parts of adjacent surfaces. the
second case will have other elements as well which are defined somewhere
else within the main function
'''
def adiabatic_walls(idf, polygon_name, perimeter_surface_coordinates,
ceiling_height, floor_height, wall_name):
'''
Internal function which creates energyplus object for adiabatic
external walls based on horizontal coordinates. Firstly, it appends
horizontal coordinates with floor and ceiling height and than loop
through coordinates in order to pick up adjacent coordinate pairs. Wall
is formed of two top and two bottom coordinates while in the horizontal
coordinate list can be a lot of adjacent coordinates pairs
'''
# Append the perimeter coordinates with the ceiling and floor heights
ceiling_coordinates = coordinates_add_height(
ceiling_height, perimeter_surface_coordinates)
floor_coordinates = coordinates_add_height(
floor_height, perimeter_surface_coordinates)
# Loop through list of ceiling coordinates lists to extract ceiling and
# floor coordinate lists. It can be more than one list pair in case of
# presence of inner holes
for n, item in enumerate(ceiling_coordinates):
ceil_coord = ceiling_coordinates[n]
floor_coord = floor_coordinates[n]
# Loop through ceiling coordinate list up to the next to the last
# item
for i, p in enumerate(ceil_coord[:-1]):
# Calculate wall centre coordinate in 3D plane (used for
# naming)
wcc = wall_centre_coordinate(
ceil_coord[i + 1], ceil_coord[i], floor_coord[i])
# Generate the name form polygon name, wall name and centre
# coordinate
surface_name = polygon_name + '_' + wall_name + '_' + wcc
# Generate wall coordinates in format used by energyplus
coordinates = idf_wall_coordinates(i, ceil_coord, floor_coord)
# Creates shading elements which represent the adiabatic
# external wall
shading_building_detailed(idf, surface_name, coordinates)
return
# Create adiabatic external walls for non-adjacent surfaces
adiabatic_walls(idf, polygon_name, perimeter_surface_coordinates,
ceiling_height, floor_height, wall_name)
# Check if there are adjacent objects (can be more than one)
if adjacent_polygons_list:
# Loop through the list of adjacent objects
for polygon in adjacent_polygons_list:
# Slice the built block DataFrame to include only records for
# adjacent object
adjacent_polygon_df = df.loc[df['osgb'] == polygon]
# Extract polygon from the adjacent objects DataFrame
adjacent_polygon = adjacent_polygon_df['sa_polygon'].iloc[0]
# Convert polygon to shapley object
adjacent_polygon = loads(adjacent_polygon)
# Find the intersection between two polygons (it will be LineString
# or MultiLineString) and position coordinates relative to origin
part_wall_polygon = polygon_shapely.intersection(adjacent_polygon)
ajd_wall_parti_surf_coord = surface_coordinates(part_wall_polygon,
origin)
# Extract the height of an adjacent object
adjacent_height = adjacent_polygon_df['height'].iloc[0]
# Check if the ceiling height is above the height of the adjacent
# object. If not than there is no adiabatic external wall above the
# adjacent object. If yes, than check the relation of floor height
# to adjacent object height. If it is above than the whole wall is
# adiabatic external; if not than only part of the wall is external
if ceiling_height > adjacent_height:
ceil_h = ceiling_height
if floor_height > adjacent_height:
floor_h = floor_height
else:
floor_h = adjacent_height
# Creates adiabatic external walls for adjacent surfaces
adiabatic_walls(idf, polygon_name, ajd_wall_parti_surf_coord,
ceil_h, floor_h, wall_name)
return
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def wall_centre_coordinate(ceil_1, ceil_0, floor_0):
'''
Function which calculates centre point of the wall. Return the
string with centre coordinate (X,Y,Z). Useful for surface naming
particularly in partition walls due to required opposite surface wall name
'''
# List of horizontal/vertical coordinates ordered pair strings
hor = [ceil_1, ceil_0]
ver = [ceil_0, floor_0]
# Exclude Z and X coordinate from horizontal and vertical coordinates resp
hor = [item[:-1] for item in hor]
ver = [item[1:] for item in ver]
# By using shapley obtain the centre coordinate between ordered pairs
hor = LineString(hor).centroid
hor = hor.coords[0]
ver = LineString(ver).centroid
ver = ver.coords[0]
# Wall centre coordinate is created from horizontal ordered pair appended
# with Z coordinate (ver[-1]) from vertical ordered pair
wcc = hor + (ver[-1],)
# Format wall centre coordinate as a string
wcc = ['%.2f' % item for item in wcc]
wcc = '(' + '_'.join(wcc) + ')'
# Return wall centre coordinate string
return wcc
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def idf_wall_coordinates(i, ceiling_coordinates, floor_coordinates):
'''
Function which converts wall coordinates into format used by E+
(upper left, bottom left, bottom right, upper right)
'''
idf_wall_coordinates = [ceiling_coordinates[i + 1],
floor_coordinates[i + 1],
floor_coordinates[i],
ceiling_coordinates[i]]
# Return wall coordinates
return idf_wall_coordinates
# END OF FUNCTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def thermal_zones(row, df, idf, origin, zone_use_dict):
polygon = loads(row.sa_polygon)
# Polygon with removed collinear point to be used for ceiling/floor/roof
hor_polygon = row.sa_polygon_horizontal
# Convert polygon coordinates to dictionary of outer and inner (if any)
# coordinates
hor_poly_coord_dict = polygon_coordinates_dictionary(hor_polygon)
# List of horizontal surfaces coordinates (roof/floor/ceiling)
horiz_surf_coord = horizontal_surface_coordinates(
hor_poly_coord_dict, origin)
# Load the polygon which defines only external surfaces
ext_surf_polygon = loads(row.sa_polygon_exposed_wall)
# List of external surface only coordinates (ext_surf_polygon + in. rings)
ext_surf_coord = surface_coordinates(ext_surf_polygon, origin)
# List of adjacent polygons
adj_osgb_list = literal_eval(row.sa_collinear_touching)
# Retrieve required attributes from table
height = row.height
glazing_ratio = row.wwr
floors = range(int(row.nofloors))
#construction = row.construction
wall_const = row.wall_const
flat_roof_const = row.roof_const
ground_floor_const = row.floor_const
glazing_const = row.glazing_const
# def zone_use(row, zone_name, zone_use_dict, floor_no):
# """
# - Records the zone use in zone_use_dict
# - Applies "Dwell" use if none specified
# - Returns the zone use
# """
# try:
# # Find use value if column exists
# use = row["floor_{}_use".format(floor_no)]
# zone_use_dict[zone_name] = use
# # Check that the value is not blank
# if isinstance(use, float) and math.isnan(use):
# # Give the "Dwell" use if use value is not present
# zone_use_dict[zone_name] = "Dwell"
# except KeyError:
# # Give the "Dwell" use if use column is not present
# zone_use_dict[zone_name] = "Dwell"
# return zone_use_dict[zone_name]
# def adiabatic_for_shading(row, space_below_floor, space_above_floor):
# """
# Checks if the zone below/above is shading. Returns "shading" str if so,
# since the name of the zone below/above is only needed to create non-adiabatic
# floors/ceilings.
# """
# if space_below_floor != "Ground":
# floor_use_index = "_".join(space_below_floor.split("_")[1:]) + "_use"
# try:
# use_below = row[floor_use_index]
# except KeyError:
# use_below = ""
# if isinstance(use_below, str) and use_below.lower() == "shading":
# space_below_floor = "shading"
# if space_above_floor != "Outdoors":
# floor_use_index = "_".join(space_above_floor.split("_")[1:]) + "_use"
# try:
# use_above = row[floor_use_index]
# except KeyError:
# use_above = ""
# if isinstance(use_above, str) and use_above.lower() == "shading":
# space_above_floor = "shading"
# return space_below_floor, space_above_floor
########### Added features for Simstock QGIS plugin ########################
overhang_depth = row.overhang_depth
#for i in floors:
# print(row["FLOOR_{}: use".format(i)])
# Select constructions
#glazing_const = "glazing"
def set_construction(construction, element):
# TODO: this is not really needed
"""
Returns the relevant name of the building surface depending on the
construction name.
"""
if element == "ground_floor":
return "{}_solid_ground_floor".format(construction)
if element == "wall":
return "{}_wall".format(construction)
if element == "roof":
return "{}_flat_roof".format(construction)
if element == "ceiling":
# Use the following to raise an error if a certain construction cannot have more than one floor
#if construction.lower() == "const1":
# raise RuntimeError("Quincha constructions cannot have multiple floors. Check polygon '%s'" % row.osgb)
return "ceiling"#.format(construction)
if element == "ceiling_inverse":
return "ceiling_inverse"#.format(construction)
############################################################################
if len(floors) == 1:
floor_no = int(1)
zone_name = '{}_floor_{}'.format(row.osgb, floor_no)
try:
zone_use_dict[zone_name] = row["FLOOR_1: use"]
except KeyError:
zone_use_dict[zone_name] = "Dwell"
zone_floor_h = 0
space_below_floor = 'Ground'
zone_ceiling_h = height
space_above_floor = 'Outdoors'
idf.newidfobject('ZONE', Name=zone_name)
floor_const = ground_floor_const
floor(idf, zone_name, space_below_floor, horiz_surf_coord,
zone_floor_h, floor_const)
roof_const = flat_roof_const
roof_ceiling(idf, zone_name, space_above_floor,
horiz_surf_coord, zone_ceiling_h, roof_const)
zone_height = zone_ceiling_h - zone_floor_h
external_walls(idf, zone_name, floor_no, ext_surf_coord,
zone_ceiling_h, zone_floor_h, zone_height,
min_avail_height, min_avail_width_for_window,
wall_const, glazing_const, glazing_ratio, overhang_depth)
# Partition walls where adjacent polygons exist
if adj_osgb_list:
# Surface type; no sun exposure; no wind exposure
partition_const = 'partition'
# Loop through the list of adjacent objects
for adj_osgb in adj_osgb_list:
opposite_zone = adj_osgb
# Extract polygon from the adjacent objects DataFrame
adj_polygon = loads(df.loc[df['osgb'] == adj_osgb,
'sa_polygon'].values[0])
adj_height = df.loc[df['osgb'] == adj_osgb,
'height'].values[0]
# Find the intersection between two polygons (it will be
# LineString or MultiLineString) and position coordinates
# relative to origin
part_wall_polygon = polygon.intersection(adj_polygon)
adj_wall_parti_surf_coord = surface_coordinates(
part_wall_polygon, origin)
if zone_ceiling_h < adj_height + 1e-6:
partition_walls(idf, zone_name, opposite_zone,
adj_wall_parti_surf_coord,
zone_ceiling_h, zone_floor_h,
partition_const)
else:
if zone_floor_h > adj_height - 1e-6:
external_walls(idf, zone_name, floor_no,
adj_wall_parti_surf_coord,
zone_ceiling_h, zone_floor_h,
zone_height, min_avail_height,
min_avail_width_for_window,
wall_const, glazing_const,
glazing_ratio, overhang_depth)