├── .gitignore ├── Data ├── GraphConstructor.py ├── LargeFilenameDataset.py ├── VolumeDesignGraph.py ├── get_uniform_voxel.py ├── process_data.py └── process_variation_data.py ├── LICENSE.md ├── Model ├── losses.py └── models.py ├── README.md ├── images ├── Building-GAN.png └── Volumetric_Design.png ├── inference.py ├── runs └── iccv2021 │ └── checkpoints │ └── 70.ckpt ├── train.py ├── train_args.py ├── util.py ├── util_eval.py └── util_graph.py /.gitignore: -------------------------------------------------------------------------------- 1 | /Data/6types-raw_data/ 2 | /Data/6types-processed_data/ 3 | /Data/6types-raw_data_/ 4 | /Data/6types-processed_data_/ 5 | /Data/6types-raw_data__/ 6 | /Data/6types-processed_data__/ 7 | /Data/__pycache__/ 8 | /Data/get_uniform_voxel_slices.py 9 | /Data/uniform_voxel_dataset_class.py 10 | /Data/6types-raw_data.zip 11 | /Model/__pycache__/ 12 | /runs/* 13 | !/runs/iccv2021/ 14 | /.idea/ 15 | /inference*/ 16 | /__pycache__/ 17 | inference_10k.py 18 | -------------------------------------------------------------------------------- /Data/GraphConstructor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from Data.VolumeDesignGraph import VolumeDesignGraph 4 | import torch 5 | from collections import Counter 6 | 7 | 8 | class GraphConstructor: 9 | number_of_class = 6 10 | dimension_norm_factor = 100 11 | 12 | global_graph_prefix = "graph_global_" 13 | local_graph_prefix = "graph_local_" 14 | voxel_graph_prefix = "voxel_" 15 | data_id_length = 6 16 | 17 | def __init__(self, ): 18 | pass 19 | 20 | @staticmethod 21 | def get_program_unicode(n): 22 | return n["floor"], n["type"], n["type_id"] 23 | 24 | @staticmethod 25 | def one_hot_vector(_id): 26 | assert _id < GraphConstructor.number_of_class 27 | return [1 if i == _id else 0 for i in range(GraphConstructor.number_of_class)] 28 | 29 | @staticmethod 30 | def load_graph_jsons(data_id, data_dir): 31 | data_id_str = str(data_id).zfill(GraphConstructor.data_id_length) 32 | with open(os.path.join(data_dir, "global_graph_data", GraphConstructor.global_graph_prefix + data_id_str+".json")) as f: 33 | global_graph = json.load(f) # Keys: FAR, site area, Global node (Room Type, target_ratio, connections) 34 | with open(os.path.join(data_dir, "local_graph_data", GraphConstructor.local_graph_prefix + data_id_str+".json")) as f: 35 | local_graph = json.load(f) 36 | with open(os.path.join(data_dir, "voxel_data", GraphConstructor.voxel_graph_prefix + data_id_str+".json")) as f: 37 | raw_voxel_graph = json.load(f) 38 | 39 | program_graph = GraphConstructor.construct_program_graph(local_graph, global_graph) 40 | voxel_graph = GraphConstructor.construct_voxel_graph(raw_voxel_graph) 41 | 42 | return GraphConstructor.tensorfy(program_graph, voxel_graph, data_id_str) 43 | 44 | @staticmethod # not used 45 | def load_graph_from_UI(global_graph, local_graph, raw_voxel_graph, data_id): 46 | data_id_str = str(data_id).zfill(GraphConstructor.data_id_length) 47 | 48 | program_graph = GraphConstructor.construct_program_graph(local_graph, global_graph) 49 | voxel_graph = GraphConstructor.construct_voxel_graph(raw_voxel_graph) 50 | 51 | return GraphConstructor.tensorfy(program_graph, voxel_graph, data_id_str) 52 | 53 | @staticmethod 54 | def tensorfy(program_grpah, voxel_graph, data_id_str): 55 | program_floor_index = torch.tensor(program_grpah["program_floor_index"], dtype=torch.long) 56 | story_level_feature = torch.FloatTensor(program_grpah["story_level_feature"]) # N x 1 57 | program_class_feature = torch.FloatTensor(program_grpah["program_class_feature"]) 58 | program_edge = torch.tensor(program_grpah["program_edge"], dtype=torch.long) 59 | neg_same_story_program_edge = torch.tensor(program_grpah["neg_same_story_program_edge"], dtype=torch.long) 60 | neg_diff_story_program_edge = torch.tensor(program_grpah["neg_diff_story_program_edge"], dtype=torch.long) 61 | program_class_cluster = torch.tensor(program_grpah["program_class_cluster"], dtype=torch.long) 62 | program_target_ratio = torch.FloatTensor(program_grpah["program_target_ratio"]) 63 | FAR = torch.FloatTensor([program_grpah["FAR"]]) 64 | voxel_floor_index = torch.tensor(voxel_graph["voxel_floor_index"], dtype=torch.long) 65 | voxel_projection_cluster = torch.tensor(voxel_graph["voxel_projection_cluster"], dtype=torch.long) 66 | voxel_feature = torch.FloatTensor(voxel_graph["voxel_feature"]) 67 | voxel_bool = torch.FloatTensor(voxel_graph["voxel_bool"]) 68 | voxel_label = torch.FloatTensor(voxel_graph["voxel_label"]) 69 | voxel_edge = torch.tensor(voxel_graph["voxel_edge"], dtype=torch.long) 70 | 71 | # add index_select for cross-edges 72 | pfc = program_grpah["program_floor_index"] 73 | program_node_id_in_story_group = [] 74 | voxel_node_counter = Counter(voxel_graph["voxel_floor_index"]) 75 | for program_node_id, story_id in enumerate(pfc): 76 | if program_node_id-1 >= 0 and story_id == pfc[program_node_id-1]: 77 | program_node_id_in_story_group[-1].append(program_node_id) 78 | else: 79 | program_node_id_in_story_group.append([program_node_id]) 80 | 81 | cross_edge_program_index_select, cross_edge_voxel_index_select, voxel_id = [], [], 0 82 | for global_story_id, program_node_id_in_this_story_group in enumerate(program_node_id_in_story_group): 83 | number_of_voxel_in_this_story = voxel_node_counter[global_story_id] 84 | number_of_program_in_this_story = len(program_node_id_in_this_story_group) 85 | cross_edge_program_index_select += program_node_id_in_this_story_group * number_of_voxel_in_this_story 86 | for _ in range(number_of_voxel_in_this_story): 87 | cross_edge_voxel_index_select += [voxel_id] * number_of_program_in_this_story 88 | voxel_id += 1 89 | 90 | assert len(cross_edge_program_index_select) == len(cross_edge_voxel_index_select) 91 | 92 | cross_edge_program_index_select = torch.tensor(cross_edge_program_index_select, dtype=torch.long) 93 | cross_edge_voxel_index_select = torch.tensor(cross_edge_voxel_index_select, dtype=torch.long) 94 | 95 | # add label_program_index. This is for the discriminator to point the voxel label back to its corresponding program node 96 | voxel_label_program_node_index = [] 97 | 98 | for voxel_id, story_id in enumerate(voxel_graph["voxel_floor_index"]): 99 | program_unicode = (story_id, ) + voxel_graph["node_id_to_program_unicode"][voxel_id] # (floor, -1, 0) if type=-1 100 | program_node_id = program_grpah["unicode_to_node_id_map"].get(program_unicode, -1) 101 | if program_node_id != -1: 102 | assert(program_floor_index[program_node_id]) == story_id 103 | voxel_label_program_node_index.append(program_node_id) 104 | 105 | voxel_label_program_node_index = torch.tensor(voxel_label_program_node_index, dtype=torch.long) 106 | 107 | # add voxel_edge_mask to extract edges across same story in voxel graph. This is not used. 108 | voxel_edge_mask = [] 109 | for p1, p2 in zip(voxel_edge[0], voxel_edge[1]): 110 | voxel_edge_mask.append(1.0 if voxel_graph["voxel_floor_index"][p1] == voxel_graph["voxel_floor_index"][p2] else 0.0) 111 | voxel_edge_mask = torch.FloatTensor(voxel_edge_mask) 112 | 113 | graph = VolumeDesignGraph(data_id_str, program_floor_index, story_level_feature, program_class_feature, 114 | program_edge, neg_same_story_program_edge, neg_diff_story_program_edge, program_class_cluster, program_target_ratio, FAR, 115 | voxel_floor_index, voxel_projection_cluster, voxel_feature, voxel_bool, voxel_label, 116 | voxel_edge, voxel_edge_mask, cross_edge_program_index_select, cross_edge_voxel_index_select, voxel_label_program_node_index) 117 | return graph 118 | 119 | @staticmethod 120 | def construct_voxel_graph(voxel_graph): 121 | # Extract Node Feature 122 | unicode_to_node_id_map, node_id_to_program_unicode, projection_map = {}, {}, {} 123 | voxel_floor_index, voxel_feature, voxel_bool, voxel_label = [], [], [], [] 124 | for i, n in enumerate(voxel_graph["voxel_node"]): 125 | voxel_bool.append(0 if n["type"] < 0 else 1) 126 | voxel_label.append(GraphConstructor.one_hot_vector(n["type"])) 127 | voxel_feature.append([*[x / GraphConstructor.dimension_norm_factor for x in n["dimension"] + n["coordinate"]], n["weight"]]) 128 | unicode_to_node_id_map[tuple(n["location"])] = i 129 | node_id_to_program_unicode[i] = (n["type"], n["type_id"]) 130 | voxel_floor_index.append(n["location"][0]) 131 | 132 | horizontal_location = (n["location"][2], n["location"][1]) # x, y 133 | if projection_map.get(horizontal_location, None): 134 | projection_map.get(horizontal_location).append(i) 135 | else: 136 | projection_map[horizontal_location] = [i] 137 | 138 | voxel_projection_cluster = [-1] * len(voxel_graph["voxel_node"]) 139 | for i, v in enumerate(projection_map.values()): 140 | for n_i in v: 141 | voxel_projection_cluster[n_i] = i 142 | assert(-1 not in voxel_projection_cluster) 143 | 144 | # Extract Edge Feature <- Already bi-directional 145 | voxel_edge_src, voxel_edge_dst = [], [] 146 | for i, n_i in enumerate(voxel_graph["voxel_node"]): 147 | for unicode_j in n_i["neighbors"]: 148 | voxel_edge_src.append(i) 149 | voxel_edge_dst.append(unicode_to_node_id_map[tuple(unicode_j)]) 150 | voxel_edge = [voxel_edge_src, voxel_edge_dst] 151 | 152 | return { 153 | "voxel_floor_index": voxel_floor_index, # dict (F --> nodes in each floor) 154 | "voxel_projection_cluster": voxel_projection_cluster, # N x 1 155 | "voxel_feature": voxel_feature, # N x 7 156 | "voxel_bool": voxel_bool, # N x 1 157 | "voxel_label": voxel_label, # N x C 158 | "voxel_edge": voxel_edge, # 2 x E 159 | "node_id_to_program_unicode": node_id_to_program_unicode # voxel node id -> type, type id 160 | } 161 | 162 | @staticmethod 163 | def construct_program_graph(local_graph, global_graph): 164 | # Extract Node Feature 165 | unicode_to_node_id_map = {} 166 | program_floor_index, story_level_feature, program_class_feature, program_class_cluster = [], [], [], [] 167 | for i, n in enumerate(local_graph["node"]): 168 | # n is a dict. Keys: floor, type, type_id 169 | unicode = GraphConstructor.get_program_unicode(n) 170 | unicode_to_node_id_map[unicode] = i 171 | # n["region_far"] and n["center"] are not used 172 | 173 | story_level_feature.append(n["floor"]) 174 | program_class_feature.append(GraphConstructor.one_hot_vector(n["type"])) 175 | program_class_cluster.append(n["type"]) 176 | program_floor_index.append(n["floor"]) 177 | 178 | # Extract Edge Feature <- Already bi-directional 179 | program_edge_src, program_edge_dst, adj_matrix = [], [], [[0 for _ in range(len(local_graph["node"]))] for _ in range(len(local_graph["node"]))] 180 | for i, n_i in enumerate(local_graph["node"]): 181 | for unicode_j in n_i["neighbors"]: 182 | program_edge_src.append(i) 183 | program_edge_dst.append(unicode_to_node_id_map[tuple(unicode_j)]) 184 | adj_matrix[i][unicode_to_node_id_map[tuple(unicode_j)]] = 1 185 | program_edge = [program_edge_src, program_edge_dst] 186 | 187 | # Construct Negative edges on the same story 188 | neg_same_story_program_edge_src, neg_same_story_program_edge_dst, neg_diff_story_program_edge_src, neg_diff_story_program_edge_dst = [], [], [], [] 189 | for i in range(len(local_graph["node"])): 190 | for j in range(i+1, len(local_graph["node"])): 191 | if adj_matrix[i][j] == 1: 192 | continue 193 | if story_level_feature[i] == story_level_feature[j]: 194 | neg_same_story_program_edge_src += [i, j] 195 | neg_same_story_program_edge_dst += [j, i] 196 | else: 197 | neg_diff_story_program_edge_src += [i, j] 198 | neg_diff_story_program_edge_dst += [j, i] 199 | neg_same_story_program_edge = [neg_same_story_program_edge_src, neg_same_story_program_edge_dst] 200 | neg_diff_story_program_edge = [neg_diff_story_program_edge_src, neg_diff_story_program_edge_dst] 201 | 202 | # Global Feature and Edge 203 | program_target_ratio = [0] * GraphConstructor.number_of_class 204 | FAR = global_graph["far"] 205 | for c in global_graph["global_node"]: 206 | program_target_ratio[c["type"]] = c["proportion"] 207 | 208 | return { 209 | "program_floor_index": program_floor_index, # N x 1 210 | "story_level_feature": story_level_feature, # N x 1 211 | "program_class_feature": program_class_feature, # N x C 212 | "program_edge": program_edge, # 2 x E 213 | "neg_same_story_program_edge": neg_same_story_program_edge, # 2 x E 214 | "neg_diff_story_program_edge": neg_diff_story_program_edge, # 2 x E 215 | "program_class_cluster": program_class_cluster, # N x 1 216 | "program_target_ratio": program_target_ratio, # C 217 | "FAR": FAR, # 1 218 | "unicode_to_node_id_map": unicode_to_node_id_map # floor, type, type_id -> program node id 219 | } 220 | 221 | -------------------------------------------------------------------------------- /Data/LargeFilenameDataset.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import os 3 | from torch.utils.data import Dataset 4 | 5 | 6 | class LargeFilenameDataset(Dataset): 7 | def __init__(self, data_dir, fname_list): 8 | self.data_dir = data_dir 9 | self.fname_list = fname_list 10 | 11 | def __len__(self): 12 | return len(self.fname_list) 13 | 14 | def __getitem__(self, i): 15 | return torch.load(os.path.join(self.data_dir, self.fname_list[i])) 16 | -------------------------------------------------------------------------------- /Data/VolumeDesignGraph.py: -------------------------------------------------------------------------------- 1 | from torch_geometric.data import Data 2 | 3 | 4 | class VolumeDesignGraph(Data): 5 | def __init__(self, data_id_str=None, program_floor_cluster=None, story_level_feature=None, program_class_feature=None, 6 | program_edge=None, neg_same_story_program_edge=None, neg_diff_story_program_edge=None, program_class_cluster=None, program_target_ratio=None, FAR=None, 7 | voxel_floor_cluster=None, voxel_projection_cluster=None, voxel_feature=None, voxel_bool=None, voxel_label=None, 8 | voxel_edge=None, voxel_edge_mask=None, cross_edge_program_index_select=None, cross_edge_voxel_index_select=None, voxel_label_program_node_index=None): 9 | """ 10 | data_id_str: (str) data ID 11 | 12 | program_edge: (e_program x 2) program edges (include same_story and diff_story) 13 | story_level_feature: (n_program_node x 1) the story level for each program nodes 14 | program_class_feature: (n_program_node x d) one-hot vector of program type 15 | program_class_cluster: (n_program_type x n_element) list of program node IDs on the same program type 16 | program_floor_cluster: (n_cluster x n_element) list of program node IDs on the same story 17 | program_target_ratio: (n_program_type x 1) the target ratio between all program types 18 | FAR: (1) target floor area ratio 19 | 20 | voxel_feature: (n_voxel x d) voxel feature 21 | voxel_floor_cluster: (n_cluster x n_element) list of voxel node IDs on the same story 22 | voxel_projection_cluster: (n_cluster x n_element) list of voxel node IDs on the same x,y coordinates 23 | voxel_bool: (n_voxel x 1) if this voxel is void 24 | voxel_label: (n_voxel x program_type) which program type is this voxel 25 | voxel_edge: (e_voxel x 2) voxel edges 26 | voxel_edge_mask: (e_voxel x 1) Not used! if the two voxel nodes on the voxel edge is on the same story 27 | 28 | cross_edge_program_index_select: (e_cross x 1) the program node IDs of the cross edges 29 | cross_edge_voxel_index_select: (e_cross x 1) the voxel node IDS of the cross edges 30 | voxel_label_program_node_index: (n_voxel x 1) which program node ID is assigned to this voxel 31 | """ 32 | 33 | super(VolumeDesignGraph, self).__init__() 34 | self.data_id_str = data_id_str 35 | self.program_floor_cluster = program_floor_cluster 36 | self.story_level_feature, self.program_class_feature = story_level_feature, program_class_feature 37 | self.program_edge, self.neg_same_story_program_edge, self.neg_diff_story_program_edge = program_edge, neg_same_story_program_edge, neg_diff_story_program_edge 38 | self.program_class_cluster = program_class_cluster 39 | self.program_target_ratio = program_target_ratio 40 | self.FAR = FAR 41 | 42 | self.voxel_floor_cluster = voxel_floor_cluster 43 | self.voxel_projection_cluster = voxel_projection_cluster 44 | self.voxel_feature = voxel_feature 45 | self.voxel_bool, self.voxel_label = voxel_bool, voxel_label 46 | self.voxel_edge = voxel_edge 47 | self.voxel_edge_mask = voxel_edge_mask 48 | 49 | self.cross_edge_program_index_select = cross_edge_program_index_select 50 | self.cross_edge_voxel_index_select = cross_edge_voxel_index_select 51 | self.voxel_label_program_node_index = voxel_label_program_node_index 52 | 53 | self._batch_program_inc_keys = ["program_edge", "cross_edge_program_index_select", "voxel_label_program_node_index", "neg_same_story_program_edge", "neg_diff_story_program_edge"] 54 | self._batch_voxel_inc_keys = ["voxel_edge", "cross_edge_voxel_index_select"] 55 | self._floor_inc_keys = ["program_floor_cluster", "voxel_floor_cluster"] 56 | self._program_class_inc_keys = ["program_class_cluster"] 57 | self._voxel_projection_inc_keys = ["voxel_projection_cluster"] 58 | self._cat_dim_keys = ["program_edge", "voxel_edge", "neg_same_story_program_edge", "neg_diff_story_program_edge"] 59 | 60 | def __inc__(self, key, value): 61 | if key in self._batch_program_inc_keys: 62 | return self.program_class_feature.size(0) # number of edges in program graph 63 | if key in self._batch_voxel_inc_keys: 64 | return self.voxel_feature.size(0) # number of nodes in voxel graph 65 | if key in self._floor_inc_keys: 66 | return int(max(self.story_level_feature) + 1) # number of stories in each volume design graph 67 | if key in self._program_class_inc_keys: 68 | return self.program_class_feature.size(1) # number of program class 69 | if key in self._voxel_projection_inc_keys: 70 | return int(max(self.voxel_projection_cluster) + 1) # number of voxels on XY plane 71 | else: 72 | return super(VolumeDesignGraph, self).__inc__(key, value) 73 | 74 | def __cat_dim__(self, key, value): 75 | # `*index*` and `*face*` should be concatenated in the last dimension, 76 | # everything else in the first dimension. 77 | return -1 if key in self._cat_dim_keys else 0 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Data/get_uniform_voxel.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | from pathlib import Path 4 | import json 5 | 6 | voxel_dir = "6types-raw_data/voxel_data" 7 | uni_voxel_label_dir = "6types-raw_data_uniform/uni_voxel_label" 8 | Path(uni_voxel_label_dir).mkdir(parents=True, exist_ok=True) 9 | uni_voxel_id_dir = "6types-raw_data_uniform/uni_voxel_id" 10 | Path(uni_voxel_id_dir).mkdir(parents=True, exist_ok=True) 11 | 12 | k = 120000 13 | 14 | for i in range(k): 15 | 16 | print(i, "/120000") 17 | 18 | with open(voxel_dir + '/voxel_{}.json'.format(str(i).zfill(5))) as f: 19 | voxels = json.load(f)["voxel_node"] 20 | 21 | uni_voxels_label = np.ones((50, 40, 40)) 22 | uni_voxels_label = - uni_voxels_label 23 | 24 | uni_voxels_id = np.zeros_like(uni_voxels_label) 25 | 26 | for id, voxel in enumerate(voxels): 27 | 28 | sz = np.array(range(int(voxel["coordinate"][0]), int(voxel["coordinate"][0] + voxel["dimension"][0]))) 29 | sy = np.array(range(int(voxel["coordinate"][1]), int(voxel["coordinate"][1] + voxel["dimension"][1]))) 30 | sx = np.array(range(int(voxel["coordinate"][2]), int(voxel["coordinate"][2] + voxel["dimension"][2]))) 31 | uni_voxels_label[np.ix_(sz, sy, sx)] = voxel["type"] 32 | uni_voxels_id[np.ix_(sz, sy, sx)] = id 33 | 34 | uni_voxels_label_tensor = torch.from_numpy(uni_voxels_label) 35 | uni_voxels_label_tensor.type(torch.DoubleTensor) 36 | 37 | uni_voxels_id_tensor = torch.from_numpy(uni_voxels_id) 38 | uni_voxels_id_tensor.type(torch.long) 39 | 40 | torch.save(uni_voxels_label_tensor, uni_voxel_label_dir + "/uni_voxel_label_{}.pt".format(str(i).zfill(6))) 41 | torch.save(uni_voxels_id_tensor, uni_voxel_id_dir + "/uni_voxel_id_{}.pt".format(str(i).zfill(6))) 42 | 43 | -------------------------------------------------------------------------------- /Data/process_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Folder structure: 3 | raw_data 4 | - global_graph_data 5 | - local_graph_data 6 | - voxel_data 7 | 8 | """ 9 | import torch 10 | from Data.GraphConstructor import GraphConstructor 11 | import os 12 | 13 | raw_data_dir = "6types-raw_data" 14 | output_dir = "6types-processed_data" 15 | global_graph_dir = os.path.join(raw_data_dir, "global_graph_data") 16 | local_graph_dir = os.path.join(raw_data_dir, "local_graph_data") 17 | voxel_graph_dir = os.path.join(raw_data_dir, "voxel_data") 18 | 19 | 20 | for fname in os.listdir(global_graph_dir): 21 | data_id = int(''.join(filter(str.isdigit, fname))) 22 | try: 23 | g = GraphConstructor.load_graph_jsons(data_id, raw_data_dir) 24 | output_fname = "data" + str(data_id).zfill(GraphConstructor.data_id_length) + ".pt" 25 | torch.save(g, os.path.join(output_dir, output_fname)) 26 | except: 27 | print("Error loading data " + str(data_id).zfill(GraphConstructor.data_id_length)) 28 | 29 | print("Data processing completed") 30 | 31 | -------------------------------------------------------------------------------- /Data/process_variation_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code generates variation samples by fixing the graph data and varying the random variable z. 3 | """ 4 | 5 | from Data.GraphConstructor import GraphConstructor 6 | import os 7 | import json 8 | import torch 9 | from shutil import copyfile 10 | 11 | 12 | num_VG = 1 13 | num_z = 10 14 | 15 | num_of_data = 1000 16 | data_dir = "./6types-raw_data/sum" 17 | output_dir = "./6types-processed-variation-v1-z10_data" 18 | os.mkdir(output_dir) 19 | 20 | raw_dir = "./6types-raw-variation-v1-z10_data" 21 | raw_voxel = raw_dir + "/voxel_data" 22 | raw_global = raw_dir + "/global_graph_data" 23 | raw_local = raw_dir + "/local_graph_data" 24 | os.mkdir(raw_dir) 25 | os.mkdir(raw_voxel) 26 | os.mkdir(raw_local) 27 | os.mkdir(raw_global) 28 | 29 | voxel_dir = "./variation_test" 30 | 31 | cnt = 0 32 | for data_id in range(num_of_data): 33 | print("generate " + str(num_VG*num_z) + "data from Data" + str(data_id)) 34 | data_id_str = str(96000 + data_id).zfill(GraphConstructor.data_id_length) 35 | with open(os.path.join(data_dir, "global_graph_data", GraphConstructor.global_graph_prefix + data_id_str + ".json")) as f: 36 | global_graph = json.load(f) # Keys: FAR, site area, Global node (Room Type, target_ratio, connections) 37 | with open(os.path.join(data_dir, "local_graph_data", GraphConstructor.local_graph_prefix + data_id_str + ".json")) as f: 38 | local_graph = json.load(f) 39 | program_graph = GraphConstructor.construct_program_graph(local_graph, global_graph) 40 | 41 | num_of_story = max(program_graph["story_level_feature"]) + 1 # starting from 1 ~ 11 42 | voxel_graph_dir = os.path.join(voxel_dir, str(num_of_story), "voxel_data") 43 | for i in range(num_VG): 44 | with open(os.path.join(voxel_graph_dir, GraphConstructor.voxel_graph_prefix + str(i).zfill(GraphConstructor.data_id_length) + ".json")) as f: 45 | raw_voxel_graph = json.load(f) 46 | voxel_graph = GraphConstructor.construct_voxel_graph(raw_voxel_graph) 47 | 48 | for _ in range(num_z): 49 | g = GraphConstructor.tensorfy(program_graph, voxel_graph, str(cnt).zfill(GraphConstructor.data_id_length)) 50 | output_fname = "data" + str(cnt).zfill(GraphConstructor.data_id_length) + ".pt" 51 | torch.save(g, os.path.join(output_dir, output_fname)) 52 | 53 | # copy json files and rename them based on current cnt 54 | global_copy_f = os.path.join(data_dir, "global_graph_data", GraphConstructor.global_graph_prefix + data_id_str + ".json") 55 | local_copy_f = os.path.join(data_dir, "local_graph_data", GraphConstructor.local_graph_prefix + data_id_str + ".json") 56 | voxel_copy_f = os.path.join(voxel_graph_dir, GraphConstructor.voxel_graph_prefix + str(i).zfill(GraphConstructor.data_id_length) + ".json") 57 | 58 | global_paste_f = os.path.join(raw_global, "graph_global_"+str(cnt).zfill(GraphConstructor.data_id_length)+".json") 59 | local_paste_f = os.path.join(raw_local, "graph_local_"+str(cnt).zfill(GraphConstructor.data_id_length)+".json") 60 | voxel_paste_f = os.path.join(raw_voxel, "voxel_" + str(cnt).zfill(GraphConstructor.data_id_length) + ".json") 61 | 62 | copyfile(global_copy_f, global_paste_f) 63 | copyfile(local_copy_f, local_paste_f) 64 | copyfile(voxel_copy_f, voxel_paste_f) 65 | 66 | cnt += 1 67 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | -------------------------------------------------------------------------------- /Model/losses.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch_geometric as tg 4 | from util import * 5 | from util_graph import get_program_ratio 6 | 7 | 8 | class VolumetricDesignLoss_D(nn.Module): 9 | def __init__(self, gan_loss, gp_lambda=10): 10 | super(VolumetricDesignLoss_D, self).__init__() 11 | self.gan_loss = gan_loss 12 | self.gp_lambda = gp_lambda 13 | 14 | def forward(self, real_validity_voxel, fake_validity_voxel, gp=None): 15 | if self.gan_loss == "WGANGP": 16 | Dv_loss = -torch.mean(real_validity_voxel[0]) - torch.mean(real_validity_voxel[1]) + torch.mean(fake_validity_voxel[0]) + torch.mean(fake_validity_voxel[1]) 17 | return Dv_loss + (self.gp_lambda * gp if gp is not None else 0) 18 | 19 | device = real_validity_voxel[0].get_device() 20 | valid0 = torch.FloatTensor(real_validity_voxel[0].shape[0], 1).fill_(1.0).to(device) 21 | valid1 = torch.FloatTensor(real_validity_voxel[1].shape[0], 1).fill_(1.0).to(device) 22 | fake0 = torch.FloatTensor(fake_validity_voxel[0].shape[0], 1).fill_(0.0).to(device) 23 | fake1 = torch.FloatTensor(fake_validity_voxel[1].shape[0], 1).fill_(0.0).to(device) 24 | 25 | if self.gan_loss == "NSGAN": # NS GAN log(D(x))+log(1-D(G(z))) 26 | loss = nn.BCELoss() 27 | return loss(real_validity_voxel[0], valid0) + loss(real_validity_voxel[1], valid1) + loss(fake_validity_voxel[0], fake0) + loss(fake_validity_voxel[1], fake1) 28 | elif self.gan_loss == "LSGAN": # LS GAN (D(x)-1)^2 + (D(G(z)))^2 29 | loss = nn.MSELoss() 30 | return 0.5 * (loss(real_validity_voxel[0], valid0) + loss(real_validity_voxel[1], valid1) + loss(fake_validity_voxel[0], fake0) + loss(fake_validity_voxel[1], fake1)) 31 | elif self.gan_loss == "hinge": # SA GAN 32 | loss = nn.ReLU() 33 | return loss(1.0 - real_validity_voxel[0]).mean() + loss(1.0 - real_validity_voxel[1]).mean() + loss(fake_validity_voxel[0] + 1.0).mean() + loss(fake_validity_voxel[1] + 1.0).mean() 34 | else: 35 | raise TypeError("self.gan_loss is not valid") 36 | 37 | 38 | class VolumetricDesignLoss_G(nn.Module): 39 | def __init__(self, lp_weight, tr_weight, far_weight, embedding_dim, sample_size, similarity_fun, gan_loss, lp_loss, hinge_margin): # hidden_dim = 128 40 | super(VolumetricDesignLoss_G, self).__init__() 41 | self.gan_loss = gan_loss 42 | self.tr_weight = tr_weight 43 | self.lp_weight = lp_weight 44 | self.far_weight = far_weight 45 | 46 | def forward(self, fake_validity_voxel, graph, att, mask, area_index_in_voxel_feature): 47 | device = att.get_device() if att.is_cuda else "cpu" 48 | target0 = torch.FloatTensor(fake_validity_voxel[0].shape[0], 1).fill_(1.0).to(device) 49 | target1 = torch.FloatTensor(fake_validity_voxel[1].shape[0], 1).fill_(1.0).to(device) 50 | 51 | # adversarial loss 52 | if self.gan_loss == "WGANGP": 53 | adversarial_loss_voxel = -torch.mean(fake_validity_voxel[0])-torch.mean(fake_validity_voxel[1]) 54 | adversarial_loss = adversarial_loss_voxel # + adversarial_loss_program 55 | elif self.gan_loss == "NSGAN": 56 | loss = nn.BCELoss() 57 | adversarial_loss = loss(fake_validity_voxel[0], target0) + loss(fake_validity_voxel[1], target1) 58 | elif self.gan_loss == "LSGAN": 59 | loss = nn.MSELoss() 60 | adversarial_loss = loss(fake_validity_voxel[0], target0) + loss(fake_validity_voxel[1], target1) 61 | elif self.gan_loss == "hinge": 62 | adversarial_loss = -torch.mean(fake_validity_voxel[0])-torch.mean(fake_validity_voxel[1]) 63 | 64 | # auxiliary loss 65 | normalized_program_class_weight, _, FAR = get_program_ratio(graph, att, mask, area_index_in_voxel_feature) 66 | target_ratio_loss = torch.Tensor([0]).to(device) if self.tr_weight == 0 else self.tr_weight * nn.functional.smooth_l1_loss(normalized_program_class_weight.flatten(), graph.program_target_ratio) 67 | far_loss = torch.Tensor([0]).to(device) if self.far_weight == 0 else self.far_weight * nn.functional.smooth_l1_loss(FAR.view(FAR.shape[0]), graph.FAR) 68 | total_loss = adversarial_loss + target_ratio_loss + far_loss 69 | 70 | return total_loss, adversarial_loss, target_ratio_loss, far_loss 71 | 72 | 73 | def compute_gradient_penalty(Dv, batch, label, out): 74 | # Interpolated sample 75 | device = out.get_device() 76 | u = torch.FloatTensor(label.shape[0], 1).uniform_(0, 1).to(device) # weight between model and gt label 77 | mixed_sample = torch.autograd.Variable(label * u + out * (1 - u), requires_grad=True).to(device) # Nv x C 78 | mask = (mixed_sample.max(dim=-1)[0] != 0).type(torch.float32).view(-1, 1) 79 | sample = softmax_to_hard(mixed_sample, -1) * mask 80 | 81 | # compute gradient penalty 82 | dv_loss = Dv(batch, sample) 83 | grad_b = torch.autograd.grad(outputs=dv_loss[0], inputs=sample, grad_outputs=torch.ones(dv_loss[0].shape).to(device), retain_graph=True, create_graph=True, only_inputs=True)[0] 84 | grad_s = torch.autograd.grad(outputs=dv_loss[1], inputs=sample, grad_outputs=torch.ones(dv_loss[1].shape).to(device), retain_graph=True, create_graph=True, only_inputs=True)[0] 85 | dv_gp_b = ((grad_b.norm(2, 1) - 1) ** 2).mean() 86 | dv_gp_s = ((grad_s.norm(2, 1) - 1) ** 2).mean() 87 | dv_gp = dv_gp_b + dv_gp_s 88 | 89 | return dv_gp, dv_gp_b, dv_gp_s 90 | 91 | 92 | -------------------------------------------------------------------------------- /Model/models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from torch_geometric.nn import MessagePassing, inits 4 | import torch_geometric as tg 5 | from Data.GraphConstructor import GraphConstructor 6 | from torch_scatter import scatter, scatter_max 7 | from util import gumbel_softmax, softmax_to_hard 8 | from util_graph import find_max_out_program_index 9 | import math 10 | 11 | 12 | # ---- POSITION ENCODING ------------------------------- 13 | def position_encoder(d_model, max_len=20): 14 | pe = torch.zeros(max_len, d_model) 15 | position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) 16 | div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) 17 | pe[:, 0::2] = torch.sin(position * div_term) 18 | pe[:, 1::2] = torch.cos(position * div_term) 19 | return pe 20 | 21 | 22 | def get_voxel_floor_level(vfc, vbi): 23 | # floor ids are serialized in batch, need to subtract the accumulated num of node in each graph 24 | pool = -tg.nn.max_pool_x(cluster=vbi, x=-vfc, batch=torch.cuda.LongTensor([0] * vfc.shape[0]))[0] 25 | return vfc-pool.index_select(0, vbi) 26 | # --------------------------------------------------------- 27 | 28 | 29 | def MLP(dims, act=""): 30 | assert len(dims) >= 2 31 | nn_list = [] 32 | for i in range(len(dims)-1): 33 | nn_list.append(nn.Linear(dims[i], dims[i+1], bias=True)) 34 | if "leaky" in act: 35 | nn_list.append(nn.LeakyReLU(negative_slope=0.01)) 36 | elif "relu" in act: 37 | nn_list.append(nn.ReLU()) 38 | elif "tanh" in act: 39 | nn_list.append(nn.Tanh()) 40 | elif "sigmoid" in act: 41 | nn_list.append(nn.Sigmoid()) 42 | return nn.Sequential(*nn_list) 43 | 44 | 45 | def extract_pos(feature): 46 | """ Split position features and the rest""" 47 | pos = feature[:, 3:6] # 3, 4, 5 are position coordinates 48 | non_pos = torch.cat((feature[:, 0:3], feature[:, 6:]), dim=-1) 49 | return pos, non_pos 50 | 51 | 52 | class ProgramBlock(MessagePassing): 53 | def __init__(self, hidden_dim): 54 | super(ProgramBlock, self).__init__() 55 | self.aggr = 'mean' 56 | self.messageMLP = MLP([2 * hidden_dim, hidden_dim]) 57 | self.updateMLP = MLP([3 * hidden_dim, hidden_dim], act="leaky") 58 | 59 | def forward(self, feature, edge_index, class_index, class_feature, batch_index): 60 | """ 61 | feature=x, edge_index=graph.program_edge, class_index=graph.program_class_cluster, 62 | class_feature=graph.program_target_ratio, batch_index=graph.program_class_feature_batch 63 | """ 64 | kwargs = {"feature": feature, "class_index": class_index, "class_feature": class_feature, "batch_index": batch_index} 65 | return self.propagate(edge_index, size=None, **kwargs) 66 | 67 | def message(self, feature_j, feature_i=None): 68 | return self.messageMLP(torch.cat((feature_i, feature_j), dim=-1)) 69 | 70 | def update(self, aggr_out, feature=None, class_index=None, class_feature=None, batch_index=None): 71 | agg_class_feature = tg.nn.avg_pool_x(cluster=class_index, x=feature, batch=batch_index, size=GraphConstructor.number_of_class)[0] * class_feature.view(-1, 1) # r_{Cl(i)}c_{i} in equation 4 72 | pool_class_feature_on_node = agg_class_feature.index_select(0, class_index) 73 | return self.updateMLP(torch.cat((feature, aggr_out, pool_class_feature_on_node), dim=-1)) 74 | 75 | 76 | class ProgramGNN(nn.Module): 77 | def __init__(self, input_dim, hidden_dim, noise_dim, layer_num): # hidden_dim = 128 78 | super(ProgramGNN, self).__init__() 79 | self.layer_num = layer_num 80 | self.enc = MLP([input_dim + noise_dim, hidden_dim]) 81 | self.block = ProgramBlock(hidden_dim) 82 | 83 | def forward(self, cf, slf, e, cc, tr, cfb, z): 84 | """ 85 | cf=graph.program_class_feature, slf=graph.story_level_feature, e=graph.program_edge, cc=graph.program_class_cluster, 86 | tr=graph.program_target_ratio, cfb=graph.program_class_feature_batch, z=noise 87 | """ 88 | x = torch.cat([cf, slf.view(-1, 1), z], dim=-1) 89 | x = self.enc(x) 90 | for i in range(self.layer_num): 91 | x = x + self.block(feature=x, edge_index=e, class_index=cc, class_feature=tr, batch_index=cfb) 92 | return x 93 | 94 | 95 | class Attention(nn.Module): 96 | def __init__(self, hidden_dim): 97 | super(Attention, self).__init__() 98 | self.dec = MLP([hidden_dim, hidden_dim // 2, 2]) 99 | self.NN_v = nn.Linear(hidden_dim, hidden_dim) 100 | self.NN_p = nn.Linear(hidden_dim, hidden_dim) 101 | self.theta = nn.Parameter(torch.Tensor(1, hidden_dim)) 102 | inits.glorot(self.theta) 103 | 104 | def forward(self, program_graph_feature, voxel_feature, cross_edge_program_index, cross_edge_voxel_index): 105 | """ 106 | program_graph_feature=program_node_feature, program_class_feature=graph.program_class_feature, voxel_feature=voxel_node_feature, 107 | cross_edge_program_index=graph.cross_edge_program_index_select, cross_edge_voxel_index=graph.cross_edge_voxel_index_select 108 | """ 109 | soft_mask = torch.nn.functional.gumbel_softmax(self.dec(voxel_feature)) 110 | hard_mask = softmax_to_hard(soft_mask, dim=-1) 111 | out_mask = {"hard": hard_mask[:, 0].view(-1, 1), "soft": soft_mask[:, 0].view(-1, 1)} 112 | 113 | program_feature_j = program_graph_feature.index_select(0, cross_edge_program_index) 114 | voxel_feature_i = voxel_feature.index_select(0, cross_edge_voxel_index) 115 | # compute attention; equation 10 116 | att = (self.theta.view(1, -1) * torch.tanh(self.NN_v(voxel_feature_i) + self.NN_p(program_feature_j))).sum(-1) 117 | # equation 11 118 | soft_att, hard_att = gumbel_softmax(att, cross_edge_voxel_index) 119 | out_att = {"hard": hard_att.view(-1, 1), "soft": soft_att.view(-1, 1)} 120 | 121 | # equation 12 122 | weighted_program_feature = out_att["soft"] * program_graph_feature[cross_edge_program_index] 123 | new_voxel_feature = voxel_feature + out_mask["soft"] * scatter(weighted_program_feature, cross_edge_voxel_index, dim=0, dim_size=voxel_feature.shape[0], reduce="sum") 124 | 125 | return out_mask, out_att, new_voxel_feature 126 | 127 | @staticmethod 128 | def construct_output(program_class_feature, num_voxel, att, mask, cross_edge_program_index, cross_edge_voxel_index): 129 | # program node index that each voxel node has the max attention 130 | max_out_program_index = find_max_out_program_index(att["hard"] * mask["hard"].index_select(0, cross_edge_voxel_index), cross_edge_voxel_index, cross_edge_program_index, num_voxel) 131 | 132 | # If a voxel node is not masked, paste the selected program class feature (i.e. which class) to the voxel node as labels. 133 | hard_out_label = att["hard"] * program_class_feature[cross_edge_program_index] 134 | hard_out_label = scatter(hard_out_label, cross_edge_voxel_index, dim=0, dim_size=num_voxel, reduce="sum") 135 | masked_hard_label = mask["hard"] * hard_out_label 136 | 137 | soft_label = att["soft"] * program_class_feature[cross_edge_program_index] 138 | soft_label = scatter(soft_label, cross_edge_voxel_index, dim=0, dim_size=num_voxel, reduce="sum") 139 | masked_soft_label = mask["hard"] * soft_label 140 | 141 | return masked_hard_label, masked_soft_label, max_out_program_index.view(-1) 142 | 143 | 144 | class VoxelBlock(MessagePassing): 145 | def __init__(self, hidden_dim, if_agg_story_in_update=True): 146 | super(VoxelBlock, self).__init__() 147 | self.aggr = 'mean' 148 | self.if_agg_story_in_update = if_agg_story_in_update # True in VoxelGNN_G, False in VoxelGNN_D 149 | self.messageMLP = MLP([2 * hidden_dim + 3, hidden_dim]) 150 | self.updateMLP = MLP([4 * hidden_dim, hidden_dim], act="leaky") if if_agg_story_in_update else MLP([2 * hidden_dim, hidden_dim]) 151 | 152 | def forward(self, voxel_feature, edge_index, batch_index, position, floor_index): 153 | """ 154 | voxel_feature=v, edge_index=graph.voxel_edge, position=pos, floor_index=graph.voxel_floor_cluster, batch_index=graph.voxel_feature_batch 155 | cross_edge_voxel_index=graph.cross_edge_program_index_select, cross_edge_program_index=graph.cross_edge_voxel_index_select, 156 | """ 157 | kwargs = {"voxel_feature": voxel_feature, "batch_index": batch_index, "position": position, "floor_index": floor_index} 158 | return self.propagate(edge_index, size=None, **kwargs) 159 | 160 | def message(self, voxel_feature_j, voxel_feature_i=None, position_i=None, position_j=None): 161 | return self.messageMLP(torch.cat((voxel_feature_i, voxel_feature_j, position_i-position_j), dim=-1)) 162 | 163 | def update(self, aggr_out, voxel_feature=None, floor_index=None, batch_index=None): 164 | if self.if_agg_story_in_update: 165 | # concatenating story feature and building feature to the input of update MLP 166 | agg_story_feature, agg_story_feature_batch = tg.nn.avg_pool_x(cluster=floor_index, x=voxel_feature, batch=batch_index) # aggregate features on the same story 167 | agg_building_feature = tg.nn.avg_pool_x(cluster=agg_story_feature_batch, x=agg_story_feature, batch=torch.cuda.LongTensor([0] * agg_story_feature.shape[0]))[0] # aggregate all features in the graph 168 | agg_story_feature = agg_story_feature.index_select(0, floor_index) # populate the feature to each voxel node 169 | agg_building_feature = agg_building_feature.index_select(0, batch_index) # populate the feature to each voxel node 170 | return self.updateMLP(torch.cat((voxel_feature, aggr_out, agg_story_feature, agg_building_feature-agg_story_feature), dim=-1)) 171 | else: 172 | return self.updateMLP(torch.cat((voxel_feature, aggr_out), dim=-1)) 173 | 174 | 175 | class VoxelGNN_G(nn.Module): 176 | def __init__(self, input_dim, hidden_dim, noise_dim, layer_num): 177 | super(VoxelGNN_G, self).__init__() 178 | self.layer_num = layer_num 179 | self.noise_dim = noise_dim 180 | self.register_buffer('pe', position_encoder(hidden_dim, 20)) 181 | self.enc = MLP([input_dim - 3 + noise_dim, hidden_dim]) 182 | self.block = VoxelBlock(hidden_dim, if_agg_story_in_update=True) 183 | self.attention = Attention(hidden_dim) 184 | 185 | def forward(self, x, v, z, e, cep, cev, vbi, vfc, vfb): 186 | """ 187 | x=program_node_feature, v=graph.voxel_feature, e=graph.voxel_edge, cep=graph.cross_edge_program_index_select, 188 | cev=graph.cross_edge_voxel_index_select, vbi=graph.voxel_feature_batch, vfc=graph.voxel_floor_cluster, vfb=graph.voxel_feature_batch 189 | """ 190 | pos, v = extract_pos(v) 191 | v = self.enc(torch.cat((v, z), dim=-1)) if self.noise_dim != 0 else self.enc(v) 192 | v = v + self.pe[get_voxel_floor_level(vfc, vbi), :] 193 | 194 | _, _, v = self.attention(program_graph_feature=x, voxel_feature=v, cross_edge_program_index=cep, cross_edge_voxel_index=cev) 195 | 196 | for i in range(self.layer_num): 197 | # voxel GNN propagation 198 | v = v + self.block(voxel_feature=v, edge_index=e, position=pos, floor_index=vfc, batch_index=vfb) 199 | # pointer-based cross-modal module 200 | if i % 2 == 0 and i != 0: 201 | out_mask, out_att, v = self.attention(program_graph_feature=x, voxel_feature=v, cross_edge_program_index=cep, cross_edge_voxel_index=cev) 202 | 203 | return out_mask, out_att, v 204 | 205 | 206 | class VoxelGNN_D(nn.Module): 207 | def __init__(self, input_feature_dim, input_label_dim, hidden_dim, layer_num): 208 | super(VoxelGNN_D, self).__init__() 209 | self.layer_num = layer_num 210 | self.register_buffer('pe', position_encoder(hidden_dim, 20)) 211 | self.feature_enc = MLP([input_feature_dim-3, hidden_dim]) 212 | self.label_enc = MLP([input_label_dim, hidden_dim]) 213 | self.block = VoxelBlock(2*hidden_dim, if_agg_story_in_update=False) 214 | 215 | def forward(self, v, l, e, vbi, vfc, vfb): 216 | """ 217 | v=graph.voxel_feature, l=out_label or graph.voxel_label, 218 | e=graph.voxel_edge, vbi=graph.voxel_feature_batch, vfc=graph.voxel_floor_cluster, vfb=graph.voxel_feature_batch 219 | """ 220 | pos, v = extract_pos(v) 221 | v = self.feature_enc(v) 222 | v = v + self.pe[get_voxel_floor_level(vfc, vbi), :] 223 | 224 | l = self.label_enc(l) 225 | v = torch.cat([v, l], dim=-1) 226 | 227 | for i in range(self.layer_num): 228 | v = v + self.block(voxel_feature=v, edge_index=e, position=pos, floor_index=vfc, batch_index=vfb) 229 | return v 230 | 231 | 232 | class Generator(nn.Module): 233 | def __init__(self, program_input_dim, voxel_input_dim, hidden_dim, noise_dim, program_layer, voxel_layer, device): # hidden = 128 234 | super(Generator, self).__init__() 235 | self.device = device 236 | self.hidden_dim = hidden_dim 237 | self.noise_dim = noise_dim 238 | self.programGNN = ProgramGNN(program_input_dim, hidden_dim, noise_dim, program_layer) 239 | self.voxelGNN = VoxelGNN_G(voxel_input_dim, hidden_dim, 0, voxel_layer) 240 | 241 | def forward(self, graph, program_z, voxel_z): 242 | program_node_feature = self.programGNN(cf=graph.program_class_feature, slf=graph.story_level_feature, e=graph.program_edge, cc=graph.program_class_cluster, 243 | tr=graph.program_target_ratio, cfb=graph.program_class_feature_batch, z=program_z) 244 | mask, att, voxel_node_feature = self.voxelGNN(x=program_node_feature, v=graph.voxel_feature, z=voxel_z, e=graph.voxel_edge, cep=graph.cross_edge_program_index_select, 245 | cev=graph.cross_edge_voxel_index_select, vbi=graph.voxel_feature_batch, vfc=graph.voxel_floor_cluster, vfb=graph.voxel_feature_batch) 246 | 247 | out, soft_out, max_out_program_index = Attention.construct_output(program_class_feature=graph.program_class_feature, num_voxel=graph.voxel_feature.shape[0], att=att, mask=mask, 248 | cross_edge_program_index=graph.cross_edge_program_index_select, cross_edge_voxel_index=graph.cross_edge_voxel_index_select) 249 | 250 | return out, soft_out, mask, att, max_out_program_index 251 | 252 | 253 | class DiscriminatorVoxel(nn.Module): 254 | def __init__(self, voxel_input_feature_dim, voxel_input_label_dim, hidden_dim, voxel_layer, act=""): 255 | super(DiscriminatorVoxel, self).__init__() 256 | self.voxelGNN_D = VoxelGNN_D(voxel_input_feature_dim, voxel_input_label_dim, hidden_dim, voxel_layer) 257 | self.building_dec = MLP([2 * hidden_dim, hidden_dim, 1], act) 258 | self.story_dec = MLP([2 * hidden_dim, hidden_dim, 1], act) 259 | 260 | def forward(self, graph, out_label): 261 | voxel_node_feature = self.voxelGNN_D(v=graph.voxel_feature, l=out_label, e=graph.voxel_edge, vbi=graph.voxel_feature_batch, vfc=graph.voxel_floor_cluster, vfb=graph.voxel_feature_batch) 262 | story_feature = tg.nn.global_max_pool(voxel_node_feature, graph.voxel_floor_cluster) 263 | graph_feature = tg.nn.global_max_pool(voxel_node_feature, graph.voxel_feature_batch.to(out_label.get_device())) 264 | return self.building_dec(graph_feature), self.story_dec(story_feature) 265 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Building-GAN 2 | ====== 3 | 4 | Code and instructions for our paper: 5 | 6 | [Building-GAN: Graph-Conditioned Architectural Volumetric Design Generation](https://arxiv.org/abs/2104.13316), ICCV 2021. 7 | 8 | 9 | 10 | Volumetric Design Process 11 | ----- 12 | 13 | 14 | Data 15 | ------ 16 | - Download the dataset [here](https://d271velnk8wqmg.cloudfront.net/6types-raw_data.zip). 17 | - Put the subfolders and files in `raw-data` under the folder `6types-raw_data`. 18 | - Run `Data/process_data.py` to process the raw data. 19 | 20 | For the detail about how the raw data are processed, please refer the `Data/process_data.py`. 21 | 22 | In the dataset, each volumetric design comprises three json files: 23 | - Global Graph: contains the FAR, program ratios, and the associated rooms for each program type. 24 | - Local Graph: contains the bubble diagram--the type and size of each room and the connectivity between rooms 25 | - Voxel: contains the voxel graph 26 | 27 | Running pretrained models 28 | ------ 29 | 30 | For running a pre-trained model, please follow the steps below: 31 | - The pre-trained model is located at `runs/iccv2021/checkpoints/` 32 | - Run ```python inference.py``` 33 | - Check out the results in the `inference/{model}/{epch_current_time}/output` folder. 34 | - Check out the variation results from the same program graph in the `inference/{model}/{epch_current_time}/var_output*` folders. 35 | 36 | Training models 37 | ------ 38 | 39 | For training a model from scratch, please follow the steps below: 40 | - Follow the steps in Data section. 41 | - run ```python train.py ```. Customized arguments can be set according to ```train_args.py```. 42 | - Check out ```output``` and ```checkpoints``` folders for intermediate outputs and checkpoints, respectively. They are under the ```runs/run_id/``` where run_id is the serial number of the 43 | experiment. 44 | 45 | Requirements 46 | ------ 47 | - PyTorch >= 1.7.0 48 | - PyTorch Geometric 1.6.2 49 | 50 | Citation 51 | ------ 52 | ``` 53 | @article{chang2021building, 54 | title={Building-GAN: Graph-Conditioned Architectural Volumetric Design Generation}, 55 | author={Chang, Kai-Hung and Cheng, Chin-Yi and Luo, Jieliang and Murata, Shingo and Nourbakhsh, Mehdi and Tsuji, Yoshito}, 56 | booktitle={International Conference on Computer Vision}, 57 | year={2021} 58 | } 59 | ``` 60 | 61 | Contact 62 | ------ 63 | Unfortunately this repo is no longer actively maintained. 64 | If you have any question, feel free to contact Chin-Yi Cheng @chinyich or Kai-Hung Chang @kaihungc1993 65 | 66 | 67 | ## License 68 | Shield: [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] 69 | 70 | This work is licensed under a 71 | [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa]. 72 | 73 | [![CC BY-NC-SA 4.0][cc-by-nc-sa-image]][cc-by-nc-sa] 74 | 75 | [cc-by-nc-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/ 76 | [cc-by-nc-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png 77 | [cc-by-nc-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg 78 | -------------------------------------------------------------------------------- /images/Building-GAN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Building-GAN/bb8948cf1f80504a35c09de9d8593e45b35e6f5b/images/Building-GAN.png -------------------------------------------------------------------------------- /images/Volumetric_Design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Building-GAN/bb8948cf1f80504a35c09de9d8593e45b35e6f5b/images/Volumetric_Design.png -------------------------------------------------------------------------------- /inference.py: -------------------------------------------------------------------------------- 1 | from train_args import make_args 2 | args = make_args() 3 | 4 | gpu_pci_str = str(args.cuda) # single-gpu version 5 | import os 6 | gpu_pci_ids = [int(i) for i in gpu_pci_str.split(',')] 7 | cuda = len(gpu_pci_ids) > 0 8 | if_multi_gpu = len(gpu_pci_ids) > 1 9 | if cuda: 10 | os.environ['CUDA_VISIBLE_DEVICES'] = gpu_pci_str 11 | 12 | from datetime import datetime 13 | current_time = datetime.now().strftime('%b%d_%H-%M-%S') 14 | 15 | from torch_geometric.data import DataLoader, Batch 16 | 17 | from Model.models import Generator 18 | from util_eval import * 19 | from util_graph import * 20 | 21 | 22 | device_ids = list(range(torch.cuda.device_count())) if cuda else [] 23 | assert(args.batch_size % len(device_ids) == 0) 24 | print('Using GPU {}'.format(os.environ['CUDA_VISIBLE_DEVICES']) if cuda else "Using CPU") 25 | if cuda: 26 | print([torch.cuda.get_device_name(device_id) for device_id in device_ids]) 27 | print(args) 28 | device = torch.device('cuda:'+str(device_ids[0]) if cuda else 'cpu') 29 | 30 | inference_dir = "inference" 31 | trained_id = "iccv2021" 32 | epoch_id = '70' 33 | 34 | trained_file = 'runs/{}/checkpoints/{}.ckpt'.format(trained_id, epoch_id) 35 | 36 | viz_dir = os.path.join(inference_dir, trained_id, epoch_id + "_" + current_time, "output") 37 | var_viz_dir1 = os.path.join(inference_dir, trained_id, epoch_id + "_" + current_time, "var_output1") 38 | var_viz_dir2 = os.path.join(inference_dir, trained_id, epoch_id + "_" + current_time, "var_output2") 39 | 40 | mkdirs = [inference_dir, os.path.join(inference_dir, trained_id), os.path.join(inference_dir, trained_id, epoch_id + "_" + current_time), viz_dir, var_viz_dir1, var_viz_dir2] 41 | for mkdir in mkdirs: 42 | if not os.path.exists(mkdir): 43 | os.mkdir(mkdir) 44 | 45 | truncated = False 46 | if not truncated: 47 | trunc_num = 1.0 48 | else: 49 | trunc_num = 0.7 50 | 51 | # Load Data 52 | follow_batch = ['program_target_ratio', 'program_class_feature', 'voxel_feature'] 53 | train_size = args.train_size//args.batch_size * args.batch_size 54 | test_size = args.test_size//args.batch_size * args.batch_size 55 | 56 | data_fname_list = sorted(os.listdir(args.train_data_dir)) 57 | print("Total %d data: %d train / %d test" % (len(data_fname_list), train_size, test_size)) 58 | 59 | variation_test_data1 = torch.load(os.path.join(args.train_data_dir, data_fname_list[args.variation_eval_id1])) 60 | variation_test_data2 = torch.load(os.path.join(args.train_data_dir, data_fname_list[args.variation_eval_id2])) 61 | variation_test_batch1 = Batch.from_data_list([variation_test_data1 for _ in range(args.variation_num)], follow_batch) 62 | variation_test_batch2 = Batch.from_data_list([variation_test_data2 for _ in range(args.variation_num)], follow_batch) 63 | 64 | test_data_list = [] 65 | for fname in data_fname_list[train_size:train_size + test_size]: 66 | test_data_list.append(torch.load(os.path.join(args.train_data_dir, fname))) 67 | 68 | test_data_loader = DataLoader(test_data_list, follow_batch=follow_batch, batch_size=args.batch_size, shuffle=False, num_workers=args.n_cpu) 69 | program_input_dim = test_data_list[0].program_class_feature.size(-1) + 1 # data_list[0].story_level_feature.size(-1) 70 | voxel_input_dim = test_data_list[0].voxel_feature.size(-1) 71 | voxel_label_dim = test_data_list[0].voxel_label.size(-1) 72 | 73 | # Load Model 74 | generator = Generator(program_input_dim, voxel_input_dim, args.latent_dim, args.noise_dim, args.program_layer, args.voxel_layer, device).to(device) 75 | generator.load_state_dict(torch.load(trained_file), strict=False) 76 | generator.eval() 77 | 78 | # evaluate 79 | n_batches = 20 # total number of generated samples = n_batches * args.batch_size 80 | evaluate(test_data_loader, generator, args.raw_dir, viz_dir, follow_batch, device_ids, number_of_batches=n_batches,trunc=trunc_num) 81 | generate_multiple_outputs_from_batch(variation_test_batch1, args.variation_num, generator, args.raw_dir, var_viz_dir1, follow_batch, device_ids, trunc=trunc_num) 82 | generate_multiple_outputs_from_batch(variation_test_batch2, args.variation_num, generator, args.raw_dir, var_viz_dir2, follow_batch, device_ids, trunc=trunc_num) 83 | -------------------------------------------------------------------------------- /runs/iccv2021/checkpoints/70.ckpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutodeskAILab/Building-GAN/bb8948cf1f80504a35c09de9d8593e45b35e6f5b/runs/iccv2021/checkpoints/70.ckpt -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | from train_args import make_args 2 | import os 3 | args = make_args() 4 | if str(args.cuda) == -1: 5 | cuda = False 6 | else: 7 | cuda = True 8 | os.environ['CUDA_VISIBLE_DEVICES'] = str(args.cuda) 9 | 10 | import torch 11 | from datetime import datetime 12 | from tensorboardX import SummaryWriter 13 | from torch_geometric.data import DataLoader, Batch 14 | from Model.models import Generator, DiscriminatorVoxel 15 | from Model.losses import VolumetricDesignLoss_D, VolumetricDesignLoss_G, compute_gradient_penalty 16 | from util_eval import * 17 | from util_graph import * 18 | from Data.LargeFilenameDataset import LargeFilenameDataset 19 | 20 | current_time = datetime.now().strftime('%b%d_%H-%M-%S') 21 | run_id = current_time + "_" + args.comment 22 | device_ids = list(range(torch.cuda.device_count())) if cuda else [] 23 | device = torch.device('cuda:'+str(device_ids[0]) if cuda else 'cpu') 24 | print('Using GPU {}'.format(os.environ['CUDA_VISIBLE_DEVICES']) if cuda else "Using CPU") 25 | print([torch.cuda.get_device_name(device_id) for device_id in device_ids] if cuda else "Using CPU") 26 | print(run_id) 27 | print(args) 28 | 29 | 30 | run_dir = "runs" 31 | viz_dir = os.path.join(run_dir, run_id, "output") 32 | var_viz_dir1 = os.path.join(run_dir, run_id, "var_output1") 33 | var_viz_dir2 = os.path.join(run_dir, run_id, "var_output2") 34 | model_dir = os.path.join(run_dir, run_id, "checkpoints") 35 | mkdirs = [os.path.join(run_dir, run_id), viz_dir, var_viz_dir1, var_viz_dir2, model_dir] 36 | for mkdir in mkdirs: 37 | os.mkdir(mkdir) 38 | writer_train = SummaryWriter(log_dir=os.path.join(run_dir, run_id)) 39 | 40 | 41 | # Load Data 42 | data_fname_list = sorted(os.listdir(args.train_data_dir)) 43 | follow_batch = ['program_target_ratio', 'program_class_feature', 'voxel_feature'] 44 | train_size = args.train_size//args.batch_size * args.batch_size 45 | test_size = args.test_size//args.batch_size * args.batch_size 46 | # print("Total %d data: %d train / %d test" % (len(data_fname_list), train_size, len(data_fname_list)-train_size)) 47 | print("Total %d data: %d train / %d test" % (len(data_fname_list), train_size, test_size)) 48 | 49 | train_data_list = LargeFilenameDataset(args.train_data_dir, data_fname_list[:train_size]) 50 | train_data_loader = DataLoader(train_data_list, follow_batch=follow_batch, batch_size=args.batch_size, shuffle=True, num_workers=args.n_cpu) 51 | test_data_list = [torch.load(os.path.join(args.train_data_dir, fname)) for fname in data_fname_list[train_size:train_size + test_size]] 52 | test_data_loader = DataLoader(test_data_list, follow_batch=follow_batch, batch_size=20, shuffle=False, num_workers=args.n_cpu) 53 | 54 | variation_test_data1 = torch.load(os.path.join(args.train_data_dir, data_fname_list[args.variation_eval_id1])) 55 | variation_test_data2 = torch.load(os.path.join(args.train_data_dir, data_fname_list[args.variation_eval_id2])) 56 | variation_test_batch1 = Batch.from_data_list([variation_test_data1 for _ in range(args.variation_num)], follow_batch) 57 | variation_test_batch2 = Batch.from_data_list([variation_test_data2 for _ in range(args.variation_num)], follow_batch) 58 | 59 | # Load Model 60 | program_input_dim = test_data_list[0].program_class_feature.size(-1) + 1 61 | voxel_input_dim = test_data_list[0].voxel_feature.size(-1) 62 | voxel_label_dim = test_data_list[0].voxel_label.size(-1) 63 | 64 | generator = Generator(program_input_dim, voxel_input_dim, args.latent_dim, args.noise_dim, args.program_layer, args.voxel_layer, device).to(device) 65 | discriminator = DiscriminatorVoxel(voxel_input_dim, voxel_label_dim, args.latent_dim, args.voxel_layer, act="sigmoid" if args.gan_loss == "NSGAN" else "").to(device) 66 | d_loss_func = VolumetricDesignLoss_D(gan_loss=args.gan_loss, gp_lambda=args.gp_lambda).to(device) 67 | g_loss_func = VolumetricDesignLoss_G(lp_weight=args.lp_weight, tr_weight=args.tr_weight, far_weight=args.far_weight, embedding_dim=args.latent_dim, sample_size=args.lp_sample_size, similarity_fun=args.lp_similarity_fun, 68 | gan_loss=args.gan_loss, lp_loss=args.lp_loss_fun, hinge_margin=args.lp_hinge_margin).to(device) 69 | optimizer_G = torch.optim.Adam(generator.parameters(), lr=args.g_lr, betas=(args.b1, args.b2)) 70 | optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=args.d_lr, betas=(args.b1, args.b2)) 71 | 72 | 73 | for epoch in range(args.n_epochs): 74 | for i, batch in enumerate(train_data_loader): 75 | batch = batch.to(device) 76 | 77 | # --------------------- 78 | # Train Discriminator 79 | # --------------------- 80 | for p in list(discriminator.parameters()): 81 | p.requires_grad = True 82 | optimizer_D.zero_grad() 83 | 84 | # random variables 85 | program_z = torch.rand(tuple([batch.program_class_feature.shape[0], args.noise_dim])).to(device) 86 | voxel_z = torch.rand(tuple([batch.voxel_feature.shape[0], args.noise_dim])).to(device) 87 | out, soft_out, mask, att, max_out_program_index = generator(batch, program_z, voxel_z) 88 | 89 | if i % args.n_critic_d == 0: 90 | fake_validity_voxel = discriminator(detach_batch(batch), out.detach()) 91 | real_validity_voxel = discriminator(batch, batch.voxel_label) 92 | 93 | if args.gan_loss == "WGANGP" and d_loss_func.gp_lambda != 0: 94 | gp, gp_b, gp_s = compute_gradient_penalty(discriminator, detach_batch(batch), batch.voxel_label.data, soft_out.data) 95 | else: 96 | gp, gp_b, gp_s = torch.tensor(0, device=device), torch.tensor(0, device=device), torch.tensor(0, device=device) 97 | 98 | d_loss = d_loss_func(real_validity_voxel, fake_validity_voxel, gp) 99 | d_loss.backward() 100 | if i % args.n_critic_d == 0: 101 | optimizer_D.step() 102 | 103 | # ----------------- 104 | # Train Generator 105 | # ----------------- 106 | for p in list(discriminator.parameters()): 107 | p.requires_grad = False 108 | optimizer_G.zero_grad() 109 | 110 | if i % args.n_critic_g == 0: 111 | validity_G_voxel = discriminator(batch, out) 112 | g_loss, gan_loss, tr_loss, far_loss = g_loss_func(validity_G_voxel, batch, att["hard"], mask["hard"], area_index_in_voxel_feature=6) 113 | g_loss.backward() 114 | optimizer_G.step() 115 | 116 | if epoch % args.plot_period == 0: 117 | avg_fake_validity_b, avg_fake_validity_s = torch.mean(fake_validity_voxel[0]), torch.mean(fake_validity_voxel[1]) 118 | avg_real_validity_b, avg_real_validity_s = torch.mean(real_validity_voxel[0]), torch.mean(real_validity_voxel[1]) 119 | print("[Epoch %d/%d] [Batch %d/%d] [D loss: %.4f, gp: %.4f] [G loss: %.4f (%.4f, %.4f, %.4f)] [Validity Real: (%.4f, %.4f), Fake (%.4f, %.4f)]" % 120 | (epoch, args.n_epochs, i + 1, len(train_data_loader), d_loss.item(), gp.item(), g_loss.item(), gan_loss.item(), tr_loss.item(), far_loss.item(), 121 | avg_real_validity_b.item(), avg_real_validity_s.item(), avg_fake_validity_b.item(), avg_fake_validity_s.item())) 122 | writer_train.add_scalar('d_loss', d_loss, epoch) 123 | writer_train.add_scalar('_gp', gp.item(), epoch) 124 | writer_train.add_scalar('g_loss', g_loss, epoch) 125 | writer_train.add_scalar('gan_loss', gan_loss, epoch) 126 | writer_train.add_scalar('target_ratio_loss', tr_loss, epoch) 127 | writer_train.add_scalar('D(fake)', avg_fake_validity_b, epoch) 128 | writer_train.add_scalar('D(real)', avg_real_validity_b, epoch) 129 | writer_train.add_scalar('D(fake)_story', avg_fake_validity_s, epoch) 130 | writer_train.add_scalar('D(real)_story', avg_real_validity_s, epoch) 131 | writer_train.close() 132 | 133 | if epoch % args.eval_period == 0: 134 | os.mkdir(os.path.join(viz_dir, str(epoch))) 135 | os.mkdir(os.path.join(var_viz_dir1, str(epoch))) 136 | os.mkdir(os.path.join(var_viz_dir2, str(epoch))) 137 | evaluate(test_data_loader, generator, args.raw_dir, os.path.join(viz_dir, str(epoch)), follow_batch, device_ids, number_of_batches=1) 138 | generate_multiple_outputs_from_batch(variation_test_batch1, args.variation_num, generator, args.raw_dir, os.path.join(var_viz_dir1, str(epoch)), follow_batch, device_ids) 139 | generate_multiple_outputs_from_batch(variation_test_batch2, args.variation_num, generator, args.raw_dir, os.path.join(var_viz_dir2, str(epoch)), follow_batch, device_ids) 140 | save_model = os.path.join(model_dir, '{}.ckpt'.format(epoch)) 141 | torch.save(generator.state_dict(), save_model) 142 | 143 | -------------------------------------------------------------------------------- /train_args.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | 4 | def add_bool_argument(arg_parser, true_arg, false_arg, dest_var, help_str): 5 | arg_parser.add_argument(true_arg, dest=dest_var, action='store_true', help=help_str) 6 | arg_parser.add_argument(false_arg, dest=dest_var, action='store_false', help=help_str) 7 | 8 | 9 | def make_args(): 10 | parser = ArgumentParser() 11 | parser.add_argument('--cuda', dest='cuda', default='0', type=str) # -1 if not using GPU 12 | parser.add_argument('--comment', dest='comment', default='0', type=str, help='comment') 13 | 14 | # Data related 15 | parser.add_argument('--batch_size', default=8, type=int, help='size of graph batching') 16 | # add_bool_argument(parser, "--if_curriculum", "--if_curriculum_no", dest_var="if_curriculum", help_str="if use curriculum") 17 | parser.add_argument('--train_data_dir', default='Data/6types-processed_data', type=str, help='where to load the training data if not preload') 18 | parser.add_argument('--raw_dir', default='Data/6types-raw_data', type=str, help='For evaluation to copy and paste') # normal runs 19 | # parser.add_argument('--preload_dir', default='Data/6types_preload_data.pkl', type=str, help='where to load the training data') 20 | 21 | # 96000, 96005, 96003 22 | parser.add_argument('--train_size', default=96000, type=int, help='how many data are train data') # 23 | parser.add_argument('--test_size', default=4000, type=int, help='how many data are test data') # 24 | parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation") # 8, if RAM OOM -> decrease to 4 25 | parser.add_argument("--variation_eval_id1", type=int, default=96018, help="Data index for variation test") # 96008 26 | parser.add_argument("--variation_eval_id2", type=int, default=96010, help="Data index for variation test") 27 | parser.add_argument("--variation_num", type=int, default=25, help="How many variation to generate for the variation test data") 28 | 29 | # ------------------------------------------------------------------------------------------------- 30 | 31 | # Model related 32 | parser.add_argument("--latent_dim", type=int, default=128, help="dimensionality of the latent space") 33 | parser.add_argument("--noise_dim", type=int, default=32, help="dimensionality of the noise space") 34 | parser.add_argument("--program_layer", type=int, default=4, help="numbers of message passing in program graph") 35 | parser.add_argument("--voxel_layer", type=int, default=12, help="numbers of message passing in voxel graph") 36 | 37 | # Loss related 38 | parser.add_argument('--gan_loss', default='WGANGP', type=str, help='WGANGP/NSGAN/LSGAN/hinge') 39 | parser.add_argument("--gp_lambda", type=float, default=10.0, help="gradient penalty coefficient in D loss") 40 | parser.add_argument("--lp_weight", type=float, default=0.0, help="link prediction coefficient in G loss") 41 | parser.add_argument("--tr_weight", type=float, default=0.0, help="program target ratio coefficient in G loss") 42 | parser.add_argument("--far_weight", type=float, default=0.0, help="far coefficient in G loss") 43 | 44 | # Link prediction related 45 | # link prediction is not used in this implementation 46 | parser.add_argument("--lp_sample_size", type=int, default=20, help="link prediction sample size") 47 | parser.add_argument('--lp_similarity_fun', default='cos', type=str, help='link prediction similarity type: cos/dot/l2/mlp') 48 | parser.add_argument('--lp_loss_fun', default='hinge', type=str, help='link prediction loss type: hinge/BCE/skipgram') 49 | parser.add_argument("--lp_hinge_margin", type=float, default=1.0, help="link prediction hinge loss margin") 50 | 51 | # Training parameter 52 | parser.add_argument("--n_epochs", type=int, default=1000, help="number of epochs of training") 53 | parser.add_argument("--n_critic_d", type=int, default=1, help="number of training steps for discriminator per iter") 54 | parser.add_argument("--n_critic_g", type=int, default=5, help="number of training steps for discriminator per iter") 55 | parser.add_argument("--n_critic_p", type=int, default=5, help="number of training steps for discriminator per iter") 56 | parser.add_argument("--plot_period", type=int, default=10, help="number of epochs between each result plot") # 10 57 | parser.add_argument("--eval_period", type=int, default=20, help="number of epochs between each evaluation and save model") # 20 58 | 59 | # Optimizer parameter 60 | parser.add_argument("--g_lr", type=float, default=0.0001, help="adam: learning rate") 61 | parser.add_argument("--d_lr", type=float, default=0.0001, help="adam: learning rate") 62 | parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient") 63 | parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient") 64 | 65 | parser.set_defaults(if_curriculum=False) 66 | args = parser.parse_args() 67 | return args 68 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch_scatter import scatter_max, scatter_add 3 | from torch_geometric.utils.num_nodes import maybe_num_nodes 4 | 5 | 6 | def softmax_to_hard(y_soft, dim): 7 | """the function uses the hard version trick as in gumbel softmax""" 8 | index = y_soft.max(dim, keepdim=True)[1] 9 | y_hard = torch.zeros_like(y_soft, memory_format=torch.legacy_contiguous_format).scatter_(dim, index, 1.0) 10 | return y_hard - y_soft.detach() + y_soft 11 | 12 | 13 | def gumbel_softmax(src, index, tau=-1, num_nodes=None): 14 | """modified from torch_geometric.utils import softmax""" 15 | num_nodes = maybe_num_nodes(index, num_nodes) 16 | 17 | gumbels = -torch.empty_like(src, memory_format=torch.legacy_contiguous_format).exponential_().log() # ~Gumbel(0,1) 18 | gumbels = (src + gumbels) / tau # ~Gumbel(logits,tau) 19 | 20 | out = gumbels - scatter_max(gumbels, index, dim=0, dim_size=num_nodes)[0][index] 21 | out = out.exp() 22 | out = out / (scatter_add(out, index, dim=0, dim_size=num_nodes)[index] + 1e-16) 23 | 24 | argmax = scatter_max(out, index, dim=0, dim_size=num_nodes)[1] 25 | out_hard = torch.zeros_like(out, memory_format=torch.legacy_contiguous_format).scatter_(0, argmax, 1.0) 26 | out_hard = out_hard - out.detach() + out 27 | 28 | return out, out_hard 29 | 30 | 31 | -------------------------------------------------------------------------------- /util_eval.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import os 3 | import json 4 | from Data.GraphConstructor import GraphConstructor 5 | from torch_geometric.data import Batch 6 | from util_graph import get_program_ratio, data_parallel, rebatch_for_multi_gpu 7 | 8 | 9 | def save_output(batch_size, batch, class_weights, program_weights, FAR, max_out_program_index, out, follow_batch, raw_dir, output_dir, new_data_id_strs=None): 10 | """ 11 | Save the evaluation results 12 | """ 13 | if not os.path.exists(os.path.join(output_dir, "global_graph_data")): 14 | os.mkdir(os.path.join(output_dir, "global_graph_data")) 15 | os.mkdir(os.path.join(output_dir, "local_graph_data")) 16 | os.mkdir(os.path.join(output_dir, "voxel_data")) 17 | 18 | num_of_program_node_accum = 0 19 | batch_all_g = [] 20 | data = rebatch_for_multi_gpu(batch, list(range(batch_size)), follow_batch, out, class_weights, program_weights, FAR, max_out_program_index) 21 | 22 | """ 23 | --- data --- 24 | g: graph 25 | o: voxel label (n[type]) 26 | cw: (n[new_proportion] in global graph) -- program class ratio/weight 27 | pw: (n[region_far] in local graph) 28 | far: (g[far] in global graph) 29 | pid: the selected program node id for each voxel node 30 | """ 31 | 32 | for i, (g, o, cw, pw, far, pid) in enumerate(data): 33 | data_id_str = g["data_id_str"][0] 34 | new_data_id_str = g["data_id_str"][0] if new_data_id_strs is None else str(new_data_id_strs[i]).zfill(GraphConstructor.data_id_length) 35 | o = o.cpu().data.numpy().tolist() 36 | cw, pw, far = cw.cpu().data.numpy(), pw.cpu().data.numpy(), far.item() 37 | 38 | # Modify Global data 39 | with open(os.path.join(raw_dir, "global_graph_data", GraphConstructor.global_graph_prefix + data_id_str + ".json")) as f: 40 | global_graph = json.load(f) 41 | global_graph["new_far"] = far 42 | for n in global_graph["global_node"]: 43 | n["new_proportion"] = float(cw[n['type']]) 44 | with open(os.path.join(output_dir, "global_graph_data", GraphConstructor.global_graph_prefix + new_data_id_str + ".json"), 'w') as f: 45 | json.dump(global_graph, f) 46 | 47 | # Modify Local data 48 | d = {} # program id to its type and type id 49 | with open(os.path.join(raw_dir, "local_graph_data", GraphConstructor.local_graph_prefix + data_id_str + ".json")) as f: 50 | local_graph = json.load(f) 51 | for i, (n, c) in enumerate(zip(local_graph["node"], pw)): 52 | n["region_far"] = float(c) 53 | d[i] = [n["type"], n["type_id"]] 54 | with open(os.path.join(output_dir, "local_graph_data", GraphConstructor.local_graph_prefix + new_data_id_str + ".json"), 'w') as f: 55 | json.dump(local_graph, f) 56 | 57 | # Modify Voxel data 58 | with open(os.path.join(raw_dir, "voxel_data", GraphConstructor.voxel_graph_prefix + data_id_str + ".json")) as f: 59 | voxel_graph = json.load(f) 60 | for n, label, _pid in zip(voxel_graph["voxel_node"], o, pid): 61 | query = d[_pid.item() - num_of_program_node_accum] 62 | n["type"] = query[0] if 1.0 in label else -1 # # label.index(1.0) 63 | n["type_id"] = query[1] if 1.0 in label else 0 64 | num_of_program_node_accum += pw.shape[0] 65 | with open(os.path.join(output_dir, "voxel_data", GraphConstructor.voxel_graph_prefix + new_data_id_str + ".json"),'w') as f: 66 | json.dump(voxel_graph, f) 67 | 68 | all_graphs = [global_graph, local_graph, voxel_graph] 69 | batch_all_g.append(all_graphs) 70 | return batch_all_g 71 | 72 | 73 | def evaluate(data_loader, generator, raw_dir, output_dir, follow_batch, device_ids, number_of_batches=0, trunc=1.0): 74 | number_of_batches = min(number_of_batches, len(data_loader)) 75 | device = device_ids[0] 76 | with torch.no_grad(): 77 | total_inter, total_program_edge = 0, 0 78 | for i, g in enumerate(data_loader): 79 | if i >= number_of_batches: 80 | break 81 | program_z_shape = [g.program_class_feature.shape[0], generator.noise_dim] 82 | program_z = torch.rand(tuple(program_z_shape)).to(device) 83 | voxel_z_shape = [g.voxel_feature.shape[0], generator.noise_dim] 84 | voxel_z = torch.rand(tuple(voxel_z_shape)).to(device) 85 | if trunc < 1.0: 86 | program_z.clamp_(min=-trunc, max=trunc) 87 | voxel_z.clamp_(min=-trunc, max=trunc) 88 | 89 | g.to(device) 90 | out, soft_out, mask, att, max_out_program_index = generator(g, program_z, voxel_z) 91 | inter_edges, missing_edges, gen_edges = check_connectivity(g, max_out_program_index, mask['hard']) 92 | total_inter += inter_edges.shape[1] 93 | total_program_edge += g.program_edge.shape[1] 94 | normalized_program_class_weight, normalized_program_weight, FAR = get_program_ratio(g, att["hard"], mask["hard"], area_index_in_voxel_feature=6) 95 | all_g = save_output(data_loader.batch_size, g, normalized_program_class_weight, normalized_program_weight,FAR, max_out_program_index, out, follow_batch, raw_dir, output_dir) 96 | 97 | acc = total_inter/total_program_edge 98 | print('acc=', acc) 99 | return all_g 100 | 101 | 102 | def check_connectivity(g, max_out_program_index, mask): 103 | """ 104 | Extract connectivity from the generated design 105 | inter_edges: program edge observed in the generated output 106 | missing_edges: program edges only in the input program graph 107 | gen_edges: program edges only in the generated output 108 | """ 109 | # Look at the g.voxel_edge and see if the two voxel nodes are masked 110 | voxel_edge_out_mask = mask.reshape([-1])[g.voxel_edge] # Ev x 2 111 | sums = torch.sum(voxel_edge_out_mask, dim=0) # Ev x 1 112 | masked_edges = g.voxel_edge[:, sums == 2] # Ev x 2 sums ==2 means voxel edges observed in the generated output 113 | 114 | if masked_edges.shape[1] != 0: 115 | # Now put program index onto the voxel edge and delete duplicates 116 | predicted_program_edges = torch.unique(max_out_program_index[masked_edges], dim=1) 117 | # union of program edges and program edges from the generated output 118 | mixed_edges = torch.cat((g.program_edge, predicted_program_edges), dim=1) 119 | unique_mix_edges, mix_counts = mixed_edges.unique(return_counts=True, dim=1) 120 | inter_edges = unique_mix_edges[:, mix_counts > 1] 121 | 122 | # program edges only in the input program graph 123 | mixed_gt_edges = torch.cat((g.program_edge, inter_edges), dim=1) 124 | unique_gt_edges, mix_gt_counts = mixed_gt_edges.unique(return_counts=True, dim=1) 125 | missing_edges = unique_gt_edges[:, mix_gt_counts == 1] 126 | 127 | # program edges only in the generated output 128 | mixed_gen_edges = torch.cat((predicted_program_edges, inter_edges), dim=1) 129 | unique_gen_edges, mix_gen_counts = mixed_gen_edges.unique(return_counts=True, dim=1) 130 | gen_edges = unique_gen_edges[:, mix_gen_counts == 1] 131 | else: # there is no voxel edge 132 | inter_edges = masked_edges 133 | missing_edges = g.program_edge 134 | gen_edges = masked_edges 135 | 136 | return inter_edges, missing_edges, gen_edges 137 | 138 | 139 | def generate_multiple_outputs_from_batch(batch, variation_num, generator, raw_dir, output_dir, follow_batch, device_ids, trunc=1.0): 140 | device = device_ids[0] 141 | batch.to(device) 142 | with torch.no_grad(): 143 | 144 | program_z_shape = [batch.program_class_feature.shape[0], generator.noise_dim] 145 | program_z = torch.rand(tuple(program_z_shape)).to(device) 146 | voxel_z_shape = [batch.voxel_feature.shape[0], generator.noise_dim] 147 | voxel_z = torch.rand(tuple(voxel_z_shape)).to(device) 148 | if trunc < 1.0: 149 | program_z.clamp_(min=-trunc, max=trunc) 150 | voxel_z.clamp_(min=-trunc, max=trunc) 151 | 152 | batch.to(device) 153 | out, soft_out, mask, att, max_out_program_index = generator(batch, program_z, voxel_z) 154 | 155 | normalized_program_class_weight, normalized_program_weight, FAR = get_program_ratio(batch, att["hard"], mask["hard"], area_index_in_voxel_feature=6) 156 | save_output(variation_num, batch, normalized_program_class_weight, normalized_program_weight, FAR, max_out_program_index, out, follow_batch, 157 | raw_dir, output_dir, new_data_id_strs=list(range(variation_num))) 158 | 159 | 160 | def generate_multiple_outputs_from_data(data, variation_num, generator, raw_dir, output_dir, follow_batch, device_ids): 161 | batch = Batch.from_data_list([data for _ in range(variation_num)], follow_batch) 162 | generate_multiple_outputs_from_batch(batch, variation_num, generator, raw_dir, output_dir, follow_batch, device_ids) 163 | 164 | -------------------------------------------------------------------------------- /util_graph.py: -------------------------------------------------------------------------------- 1 | from Data.VolumeDesignGraph import VolumeDesignGraph 2 | from torch_geometric.data import Batch 3 | from torch_scatter import scatter_add, scatter, scatter_max 4 | import torch 5 | import copy 6 | 7 | 8 | def detach_batch(batch): 9 | detached_batch = Batch() 10 | detached_batch.__data_class__ = VolumeDesignGraph 11 | detached_batch.__slices__ = copy.deepcopy(batch.__slices__) 12 | 13 | for key in batch.keys: 14 | if torch.is_tensor(batch[key]): 15 | detached_batch[key] = batch[key].detach() 16 | else: 17 | detached_batch[key] = copy.deepcopy(batch[key]) 18 | return detached_batch 19 | 20 | 21 | def get_program_ratio(graph, att, mask, area_index_in_voxel_feature): 22 | """ 23 | For each program type, we sum all the areas from the corresponding voxel nodes and obtain program weight. 24 | We can then normalize it and compute FAR. 25 | """ 26 | device = att.get_device() if att.is_cuda else "cpu" 27 | # Nv x 1 if voxel node is masked, area = 0; otherwise, area. 28 | masked_voxel_weight = mask * graph.voxel_feature[:, area_index_in_voxel_feature].view(-1, 1) 29 | # E x 1 put area values on the cross edge 30 | painted_voxel_weight = (att * torch.index_select(masked_voxel_weight, 0, graph.cross_edge_voxel_index_select)) 31 | # Np x 1 sum of voxel node areas on the program node 32 | program_weight = scatter(src=painted_voxel_weight, index=graph.cross_edge_program_index_select, dim=0, dim_size=graph.program_class_feature.shape[0], reduce="sum") 33 | # Sums the areas of program nodes for each type 34 | program_class_weight = scatter(src=program_weight, index=graph.program_class_cluster, dim=0, dim_size=graph.program_target_ratio.shape[0], reduce='sum') 35 | # Sums the total area for each graph 36 | batch_sum = scatter_add(program_class_weight, graph.program_target_ratio_batch.to(device), dim=0, dim_size=graph.FAR.shape[0])[graph.program_target_ratio_batch] 37 | # Normalize the program ratio in each graph 38 | normalized_program_class_weight = program_class_weight / (batch_sum + 1e-16) 39 | # Compute FAR 40 | FAR = scatter(src=program_class_weight, index=graph.program_target_ratio_batch, dim=0, dim_size=graph.FAR.shape[0], reduce="sum") 41 | return normalized_program_class_weight, program_weight, FAR 42 | 43 | 44 | def find_max_out_program_index(logit, cross_edge_voxel_index, cross_edge_program_index, num_of_voxels): 45 | """ max_out_program_index (Nv x 1) is the program node index that each voxel node has the max attention. We also compute voxel nodes that are masked (mask["hard"])""" 46 | _, out_cross_edge_index = scatter_max(logit, index=cross_edge_voxel_index, dim=0, dim_size=num_of_voxels) 47 | max_out_program_index = cross_edge_program_index[out_cross_edge_index] 48 | return max_out_program_index 49 | 50 | 51 | def unbatch_data_to_data_list(batch): 52 | """ 53 | Modified by torch_geometric.data.batch.to_data_list() since the keys are not in order, so when calling __inc__ and number of nodes are queried, 54 | error occurs because the node features might not be populated yet. 55 | The "xxx_batch" will be dropped in the output as in the data_list. You recreate them when making batches 56 | """ 57 | if batch.__slices__ is None: 58 | raise RuntimeError('Cannot reconstruct data list from batch because the batch object was not created using Batch.from_data_list()') 59 | keys = [key for key in batch.keys if key[-5:] != 'batch'] 60 | cumsum = {key: 0 for key in keys} 61 | data_list = [] 62 | for i in range(len(batch.__slices__[keys[0]]) - 1): 63 | data = batch.__data_class__() 64 | for key in keys: 65 | if data[key] is not None: 66 | continue 67 | if torch.is_tensor(batch[key]): 68 | data[key] = batch[key].narrow(data.__cat_dim__(key, batch[key]), batch.__slices__[key][i], batch.__slices__[key][i + 1] - batch.__slices__[key][i]) 69 | # if batch[key].dtype != torch.bool: data[key] = data[key] - cumsum[key] 70 | else: 71 | data[key] = batch[key][batch.__slices__[key][i]:batch.__slices__[key][i + 1]] 72 | # cumsum[key] = cumsum[key] + data.__inc__(key, data[key]) 73 | for key in keys: 74 | if torch.is_tensor(batch[key]) and batch[key].dtype != torch.bool: 75 | data[key] = data[key] - cumsum[key] 76 | cumsum[key] = cumsum[key] + data.__inc__(key, data[key]) 77 | 78 | data["data_id_str"] = data["data_id_str"][0] # This is added, otherwise this will be list not str 79 | data_list.append(data) 80 | return data_list 81 | 82 | 83 | def rebatch_graph_for_multi_gpu(batch, device_ids, follow_batch): 84 | """ 85 | Given a batch of data, split to multiple mini-batches. 86 | """ 87 | data_list = unbatch_data_to_data_list(batch) 88 | mini_batch_size = len(data_list)//len(device_ids) 89 | mini_batch_list, mini_batch_slices = [], [0] 90 | for i in range(len(device_ids)): 91 | mini_batch_list.append(Batch.from_data_list(data_list[i * mini_batch_size: (i+1) * mini_batch_size], follow_batch=follow_batch)) 92 | mini_batch_slices.append((i+1) * mini_batch_size) 93 | return mini_batch_list, mini_batch_slices 94 | 95 | 96 | def rebatch_for_multi_gpu(batch, device_ids, follow_batch, *args): 97 | """ 98 | This function rebatches batched graphs(batch) and other information(*args) based on the given device/new_batch ids. 99 | Return dimension [M batches x N features (list of batch and args)] 100 | 101 | split_by_graph: FAR, class_weights 102 | split_by_program_node: z noise, program_weights 103 | split_by_voxel_node: out, mask_hard, max_out_program_index 104 | 105 | example args: batch = batch, args = out, class_weights, program_weights, FAR, max_out_program_index 106 | return 107 | g: graph 108 | ------------------ 109 | o: voxel label (n[type]) 110 | cw: (n[new_proportion] in global graph) -- program class ratio/weight 111 | pw: (n[region_far] in local graph) 112 | far: (g[far] in global graph) 113 | pid: the selected program node id for each voxel node 114 | """ 115 | 116 | # First rebatch the batched graphs 117 | mini_batch_list, mini_batch_slices = rebatch_graph_for_multi_gpu(batch, device_ids, follow_batch) 118 | 119 | # Then identify batching method for the args 120 | rebatch_type_key = [] 121 | for arg in args: 122 | if arg.shape[0] == batch["FAR"].shape[0]: 123 | rebatch_type_key.append("FAR") 124 | elif arg.shape[0] == batch["program_class_feature"].shape[0]: 125 | rebatch_type_key.append("program_class_feature") 126 | elif arg.shape[0] == batch["voxel_feature"].shape[0]: 127 | rebatch_type_key.append("voxel_feature") 128 | elif arg.shape[0] == batch["program_target_ratio"].shape[0]: 129 | rebatch_type_key.append("program_target_ratio") 130 | else: 131 | raise ValueError("unknown input") 132 | 133 | # Save to the output data structure 134 | ret, data = [], batch.__data_class__() 135 | for i, (device_id, mini_batch) in enumerate(zip(device_ids, mini_batch_list)): 136 | placeholder = [mini_batch] 137 | start, end = mini_batch_slices[i], mini_batch_slices[i+1] 138 | for arg, key in zip(args, rebatch_type_key): 139 | mini_arg = arg.narrow(data.__cat_dim__(key, None), batch.__slices__[key][start], batch.__slices__[key][end] - batch.__slices__[key][start]) 140 | placeholder.append(mini_arg) 141 | try: 142 | placeholder = [ele.to(device_id) for ele in placeholder] 143 | except: 144 | pass 145 | ret.append(tuple(placeholder)) 146 | return ret 147 | 148 | 149 | def data_parallel(module, batch, _input, follow_batch, device_ids): 150 | """ 151 | Reference code for multi-gpu setups. Not used in the this code repo 152 | """ 153 | output_device = device_ids[0] 154 | replicas = torch.nn.parallel.replicate(module, device_ids) 155 | inputs = rebatch_for_multi_gpu(batch, device_ids, follow_batch, *_input) 156 | replicas = replicas[:len(inputs)] 157 | outputs = torch.nn.parallel.parallel_apply(replicas, inputs) 158 | return torch.nn.parallel.gather(outputs, output_device) 159 | --------------------------------------------------------------------------------