From 142b9f51ddd91aa3f66153db1f767dafeb92c4c8 Mon Sep 17 00:00:00 2001 From: govin08 Date: Mon, 5 Feb 2024 16:45:15 +0900 Subject: [PATCH] make generate_signals.py --- Data/networks/cmd.txt | 6 +- .../table_definition_v0.8.4.xlsx | Bin 75407 -> 75490 bytes Intermediates/node2num_cycles.json | 17 + .../새 폴더/histid_1704431700.csv | 215 ++++ .../새 폴더/histid_1704431700_.csv | 215 ++++ Results/issues_generate_signals.txt | 0 Results/issues_intermediates.txt | 1 - Results/issues_preprocess_daily.txt | 0 Results/sn_1704439800.add.xml | 716 +++++++++++++ Results/sn_1704440100.add.xml | 716 +++++++++++++ Results/sn_1704440400.add.xml | 716 +++++++++++++ Script/generate_signals.ipynb | 59 +- Script/generate_signals.py | 786 ++++++++++++++ Script/get_intermediates.py | 587 ---------- Script/get_signals.py | 587 ---------- Script/preprocess_5min.ipynb | 19 - Script/preprocess_daily.py | 999 ++++++++++-------- Script/preprocess_daily_0.py | 431 ++++++++ state_300.00.xml.gz | Bin 0 -> 3780 bytes 19 files changed, 4442 insertions(+), 1628 deletions(-) create mode 100644 Intermediates/node2num_cycles.json create mode 100644 Intermediates/새 폴더/histid_1704431700.csv create mode 100644 Intermediates/새 폴더/histid_1704431700_.csv create mode 100644 Results/issues_generate_signals.txt delete mode 100644 Results/issues_intermediates.txt create mode 100644 Results/issues_preprocess_daily.txt create mode 100644 Results/sn_1704439800.add.xml create mode 100644 Results/sn_1704440100.add.xml create mode 100644 Results/sn_1704440400.add.xml create mode 100644 Script/generate_signals.py delete mode 100644 Script/get_intermediates.py delete mode 100644 Script/get_signals.py create mode 100644 Script/preprocess_daily_0.py create mode 100644 state_300.00.xml.gz diff --git a/Data/networks/cmd.txt b/Data/networks/cmd.txt index 1dc3a09df..4ef91b015 100644 --- a/Data/networks/cmd.txt +++ b/Data/networks/cmd.txt @@ -1,3 +1,7 @@ sumo-gui -n sn.net.xml -a ../../Results/sn_1704411900.add.xml -r sn.rou.xml -d 100 --save-state.times 300 -sumo-gui -n sn.net.xml -a ../../Results/sn_1704412200.add.xml -r sn.rou.xml -d 100 --load-state state_300.00.xml.gz \ No newline at end of file +sumo-gui -n sn.net.xml -a ../../Results/sn_1704412200.add.xml -r sn.rou.xml -d 100 --load-state state_300.00.xml.gz + +sumo-gui -n Data/networks/sn.net.xml -a Results/sn_1704411900.add.xml -r Data/networks/sn.rou.xml -d 100 --save-state.times 300 + +sumo-gui -n Data/networks/sn.net.xml -a Results/sn_1704411900.add.xml -r Data/networks/sn.rou.xml -d 100 --load-state state_300.00.xml.gz \ No newline at end of file diff --git a/Documents/1127_table_definition/table_definition_v0.8.4.xlsx b/Documents/1127_table_definition/table_definition_v0.8.4.xlsx index 615f6c0d9e58d62b9edadf17ee57cddcc939fce2..1cb8323c920e1fe1d56cd7e289e823af5e5b4eb1 100644 GIT binary patch delta 27298 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 AFaQ7m delta 27190 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Results/sn_1704440100.add.xml b/Results/sn_1704440100.add.xml new file mode 100644 index 000000000..633e54bb8 --- /dev/null +++ b/Results/sn_1704440100.add.xml @@ -0,0 +1,716 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Results/sn_1704440400.add.xml b/Results/sn_1704440400.add.xml new file mode 100644 index 000000000..2714ac05d --- /dev/null +++ b/Results/sn_1704440400.add.xml @@ -0,0 +1,716 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Script/generate_signals.ipynb b/Script/generate_signals.ipynb index eba01e851..2206b1434 100644 --- a/Script/generate_signals.ipynb +++ b/Script/generate_signals.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -51,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -102,7 +102,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -172,7 +172,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -232,7 +232,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -258,7 +258,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -281,7 +281,7 @@ " 'u60': 4}" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -305,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -328,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -351,7 +351,7 @@ " 'u60': 4}" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -362,7 +362,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -410,7 +410,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -729,7 +729,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -781,6 +781,37 @@ "SIGTABLE = assign_red_yellow(Sigtable)" ] }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1577804400\n", + "2145884400\n", + "4021\n", + "4021\n" + ] + } + ], + "source": [ + "max_unix, min_unix = int(datetime(2020, 1, 1).timestamp()), int(datetime(2038, 1, 1).timestamp())\n", + "print(max_unix)\n", + "print(min_unix)\n", + "history = pd.read_csv('../Data/tables/history.csv', index_col=0)\n", + "K = 0\n", + "for _, row in history.iterrows():\n", + " unixbool = min_unix <= row['end_unix'] <= max_unix\n", + " print(min_unix, row['end_unix'], max_unix)\n", + " if not unixbool:\n", + " K += 1\n", + "print(K)\n", + "print(len(history))" + ] + }, { "cell_type": "code", "execution_count": 13, diff --git a/Script/generate_signals.py b/Script/generate_signals.py new file mode 100644 index 000000000..80b303b3b --- /dev/null +++ b/Script/generate_signals.py @@ -0,0 +1,786 @@ +import pandas as pd +import numpy as np +import os, sys +import json +import copy +from tqdm import tqdm +import sumolib, traci +from datetime import datetime + +class SignalGenerator(): + def __init__(self): + self.path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.issues = [] + + self.midnight = int(datetime(2024, 1, 5, 0, 0, 0).timestamp()) + self.next_day = int(datetime(2024, 1, 6, 0, 0, 0).timestamp()) + self.fsecs = range(self.midnight, self.next_day, 5) # fsecs : unix time by Five SECondS + self.fmins = range(self.midnight, self.next_day, 300) # fmins : unix time by Five MINuteS + + self.present_time = datetime.now().replace(month=1, day=5).timestamp() + print(self.present_time) + self.present_time = max([fmin for fmin in list(self.fmins) if fmin <= self.present_time]) + + self.adder = 600 + + # 1. 데이터 준비 + def prepare_data(self): + print("1. 데이터를 준비합니다.") + self.load_networks() + self.load_tables() + self.check_networks() + self.check_tables() + self.prepare_auxiliaries() + + # 1-1. 네트워크 불러오기 + def load_networks(self): + self.net = sumolib.net.readNet(os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')) + print("1-1. 네트워크가 로드되었습니다.") + + # 1-2. 테이블 불러오기 + def load_tables(self): + # 모든 컬럼에 대하여 데이터타입 지정 + loading_dtype = { + 'inter_no':'int', 'start_hour':'int', 'start_minute':'int', 'cycle':'int','offset':'int', + 'node_id':'str', 'inter_type':'str', 'parent_id':'str','child_id':'str', + 'direction':'str', 'condition':'str', 'inc_edge':'str', 'out_edge':'str', + 'end_unix':'int', 'inter_name':'str', 'inter_lat':'float', 'inter_lon':'float', + 'group_no':'int', 'main_phase_no':'int', 'phase_no':'int','ring_type':'str' + } + for alph in ['A', 'B']: + for j in range(1,9): + loading_dtype[f'angle_{alph}{j}'] = 'str' + loading_dtype[f'dura_{alph}{j}'] = 'int' + + self.path_table = os.path.join(self.path_root, 'Data', 'tables') + + # 테이블 불러오기 + self.inter_info = pd.read_csv(os.path.join(self.path_table, 'inter_info.csv'), dtype=loading_dtype) + self.plan = pd.read_csv(os.path.join(self.path_table, 'plan.csv'), dtype=loading_dtype) + self.history = pd.read_csv(os.path.join(self.path_table, 'history.csv'), dtype=loading_dtype) + self.inter_node = pd.read_csv(os.path.join(self.path_table, 'inter_node.csv'), dtype=loading_dtype) + self.matching = pd.read_csv(os.path.join(self.path_root, 'Intermediates', 'matching.csv'), dtype=loading_dtype) + self.movements = pd.read_csv(os.path.join(self.path_root, 'Intermediates', 'movements.csv'), dtype=loading_dtype) + self.match6 = pd.read_csv(os.path.join(self.path_root, 'Intermediates', 'match6.csv'), dtype=loading_dtype) + self.match6 = self.match6[['node_id', 'phase_no', 'ring_type', 'inc_edge', 'out_edge']].reset_index(drop=True) + + # 교차로목록 정의 + self.inter_nos = sorted(self.inter_info.inter_no.unique()) + print("1-2. 테이블들이 로드되었습니다.") + + # 1-3. 네트워크 무결성 검사 + def check_networks(self): + # https://sumo.dlr.de/docs/Netedit/neteditUsageExamples.html#simplify_tls_program_state_after_changing_connections + if 'SUMO_HOME' in os.environ: + tools = os.path.join(os.environ['SUMO_HOME'], 'tools') + if tools not in sys.path: + sys.path.append(tools) + else: + raise EnvironmentError("please declare environment variable 'SUMO_HOME'") + traci.start([sumolib.checkBinary('sumo'), "-n", os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')]) + nodes = [node for node in self.net.getNodes() if node.getType()=='traffic_light'] + for node in nodes: + node_id = node.getID() + from_xml = len([c for c in node.getConnections() if c.getTLLinkIndex() >= 0]) + from_traci = len(traci.trafficlight.getRedYellowGreenState(node_id)) + if from_xml != from_traci: + sub = {'id': node_id, 'type': 'node', 'note': '유효하지 않은 연결이있음. netedit에서 clean states 필요.'} + self.issues.append(sub) + traci.close() + print("1-3. 네트워크의 모든 clean state requirement들을 체크했습니다.") + + # 1-4. 테이블 무결성 검사 + def check_tables(self): + self.check_history() + # 교차로정보, 방위각정보, 신호계획에 대해서는 preprocess_daily.py에서 + # 무결성검사를 완료했으므로 여기에서는 따로 검사하지 않음. + # self.check_moves() # 이동류번호에 대한 무결성검사 필요하나 아직 작성하지 않음. (24. 2. 5 화) + print("1-4. 테이블들의 무결성 검사를 완료했습니다.") + + # 1-4-1. 신호이력(history) 검사 + def check_history(self): + # 1-4-1-1. inter_no 검사 + # self.history.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드 + missing_inter_nos = set(self.history.inter_no) - set(self.inter_nos) + if missing_inter_nos: + msg = f"1-4-1-1. history의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}" + self.issues.append(msg) + + # 1-4-1-2. 종료유닉스 검사 + # self.history.loc[0, 'end_unix'] = 38.0 # 에러 발생을 위한 코드 + self.min_unix, self.max_unix = int(datetime(2020, 1, 1).timestamp()), int(datetime(2038, 1, 1).timestamp()) + for _, row in self.history.iterrows(): + unixbool = self.min_unix <= row['end_unix'] <= self.max_unix + if not unixbool: + msg = f"1-4-1-2. 적정 범위를 벗어난 유닉스시각(end_unix)이 존재합니다 : inter_no : {row['inter_no']}" + self.issues.append(msg) + + # 1-4-1-3. 현시시간 검사 + # self.history.loc[0, 'dura_A1'] = -2 # 에러 발생을 위한 코드 + durations = self.history[[f'dura_{alph}{j}' for alph in ['A','B'] for j in range(1, 9)]] + valid_indices = ((durations >= 0) & (durations <= 200)).all(axis=1) + invalid_inter_nos = sorted(self.history[~ valid_indices].inter_no.unique()) + if invalid_inter_nos: + msg = f"1-4-1-3. 음수이거나 200보다 큰 현시시간이 존재합니다. : {invalid_inter_nos}" + + # 1-5. 보조 딕셔너리, 데이터프레임, 리스트 등 만들기 + def prepare_auxiliaries(self): + # inter2node : a dictionary that maps inter_no to the node_id + inter_node_p = self.inter_node[self.inter_node.inter_type=='parent'] + self.inter2node = dict(zip(inter_node_p['inter_no'], inter_node_p['node_id'])) + self.node2inter = dict(zip(self.inter_node['node_id'], self.inter_node['inter_no'])) + + # hours : 정각에 해당하는 시각들 목록 + self.hours = np.array(range(self.midnight - 7200, self.next_day + 1, 3600)) + + # split, isplit : A,B 분리 혹은 통합시 사용될 수 있는 딕셔너리 + self.splits = {} # splits maps (inter_no, start_hour, start_minute) to split + for i, row in self.plan.iterrows(): + inter_no = row.inter_no + start_hour = row.start_hour + start_minute = row.start_minute + cycle = row.cycle + cums_A = row[[f'dura_A{j}' for j in range(1,9)]].cumsum() + cums_B = row[[f'dura_B{j}' for j in range(1,9)]].cumsum() + self.splits[(inter_no, start_hour, start_minute)] = {} # split maps (phas_A, phas_B) to k + k = 0 + for t in range(cycle): + new_phas_A = len(cums_A[cums_A < t]) + 1 + new_phas_B = len(cums_B[cums_B < t]) + 1 + if k == 0 or ((new_phas_A, new_phas_B) != (phas_A, phas_B)): + k += 1 + phas_A = new_phas_A + phas_B = new_phas_B + self.splits[(inter_no, start_hour, start_minute)][(phas_A, phas_B)] = k + self.isplits = {} # the inverse of splits + for i in self.splits: + self.isplits[i] = {self.splits[i][k]:k for k in self.splits[i]} # isplit maps k to (phas_A, phas_B) + + # timetable : 교차로별 프로그램 시작시각 + self.timetable = self.plan[['start_hour', 'start_minute']].drop_duplicates() + self.timetable['start_seconds'] = self.midnight + self.timetable['start_hour'] * 3600 + self.timetable['start_minute'] * 60 + + # A dictionary that maps parent_id to a list of child_ids + self.pa2ch = {'i0':['u00'], 'i1':[], 'i2':['u20'], 'i3':['c30', 'u30', 'u31', 'u32'], 'i6':['u60'], 'i7':[], 'i8':[], 'i9':[]} + self.node_ids = sorted(self.inter_node.node_id.unique()) + self.parent_ids = sorted(self.inter_node[self.inter_node.inter_type=='parent'].node_id.unique()) + self.nodes = [self.net.getNode(node_id) for node_id in self.node_ids] + + # node2num_cycles : A dictionary that maps a node_id to the number of cycles + with open(os.path.join(self.path_root, 'Intermediates', 'node2num_cycles.json'), 'r') as file: + # json.load() 함수를 사용해 파일 내용을 Python 딕셔너리로 불러옵니다. + self.node2num_cycles = json.load(file) + + # 2. 신호이력 전처리 + def process_history(self): + print("2. 신호이력 테이블을 변환합니다.") + self.make_rhistory() + self.make_rhists() + self.make_hrhists() + + # 2-1. rhistory + def make_rhistory(self): + # 1. 조회시점의 유닉스 타임 이전의 신호이력 수집 + self.rhistory = self.history.copy() # recent history + self.rhistory = self.rhistory[(self.rhistory.end_unix <= self.present_time) & (self.rhistory.end_unix > self.present_time - 9000)] # 두 시간 반 전부터 현재까지의 신호이력을 가져옴. 9000 = 3600 * 2.5 + + # rhistory에 모든 교차로번호가 존재하지 않으면 해당 교차로번호에 대한 신호이력을 추가함 (at 최근 프로그램 시작시각) + whole_inter_nos = sorted(self.history.inter_no.unique()) + recent_inter_nos = sorted(self.rhistory.inter_no.unique()) + if not whole_inter_nos==recent_inter_nos: + for inter_no in set(whole_inter_nos) - set(recent_inter_nos): + program_start, prow = self.load_prow(inter_no, self.present_time - 9000) + cycle = prow.cycle.iloc[0] + row1 = prow.drop(['start_hour', 'start_minute'], axis=1).copy() + row2 = prow.drop(['start_hour', 'start_minute'], axis=1).copy() + # prow에서 필요한 부분을 rhistory에 추가 + row1['end_unix'] = program_start + row2['end_unix'] = program_start + cycle + self.rhistory = pd.concat([self.rhistory, row1, row2]).reset_index(drop=True) + # present_time + adder 의 시각에 한 주기의 신호 추가 + for inter_no in set(whole_inter_nos): + program_start, prow = self.load_prow(inter_no, self.present_time) + cycle = prow.cycle.iloc[0] + row3 = prow.drop(['start_hour', 'start_minute'], axis=1).copy() + # prow에서 필요한 부분을 rhistory에 추가 + row3['end_unix'] = self.present_time + self.adder + self.rhistory = pd.concat([self.rhistory, row3]).reset_index(drop=True) + + # 2. 시작 유닉스 타임컬럼 생성 후 종류 유닉스 타임에서 현시별 현시기간 컬럼의 합을 뺀 값으로 입력 + # - 현시시간의 합을 뺀 시간의 +- 10초 이내에 이전 주기정보가 존재하면 그 유닉스 시간을 시작 유닉스시간 값으로 하고, 존재하지 않으면 현시시간의 합을 뺀 유닉스 시간을 시작 유닉스 시간으로 지정 + for i, row in self.rhistory.iterrows(): + inter_no = row.inter_no + end_unix = row.end_unix + elapsed_time = row[[f'dura_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]].sum() // 2 # 현시시간 합 + # 이전 유닉스 존재하지 않음 : 현시시간 합의 차 + start_unix = end_unix - elapsed_time + pre_rows = self.history[:i] # previous rows + if inter_no in pre_rows.inter_no.unique(): # 이전 유닉스 존재 + pre_unix = pre_rows[pre_rows.inter_no == inter_no]['end_unix'].iloc[-1] # previous unix time + # 이전 유닉스 존재, abs < 10 : 이전 유닉스 + if abs(pre_unix - start_unix) < 10: + start_unix = pre_unix + # 이전 유닉스 존재, abs >=10 : 현시시간 합의 차 + else: + pass + self.rhistory.loc[i, 'start_unix'] = start_unix + self.rhistory[self.rhistory.isna()] = 0 + self.rhistory['start_unix'] = self.rhistory['start_unix'].astype(int) + self.rhistory = self.rhistory[['inter_no', 'start_unix'] + [f'dura_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)] + ['cycle']] + + def load_prow(self, inter_no, time): + ''' + load planned row + ''' + # 프로그램 시작시각 + program_starts = np.array(self.timetable.start_seconds) + idx = (program_starts <= time).sum() - 1 + program_start = program_starts[idx] + + # 최근 프로그램 시작시각에 대한 신호계획 + start_hour = self.timetable.iloc[idx].start_hour + start_minute = self.timetable.iloc[idx].start_minute + prow = self.plan[(self.plan.inter_no==inter_no) & (self.plan.start_hour==start_hour) & (self.plan.start_minute==start_minute)] # planned row + return program_start, prow + + # 2-2. rhists + def make_rhists(self): + self.rhists = [] + for inter_no in sorted(self.rhistory.inter_no.unique()): + self.rhist = self.rhistory.copy()[self.rhistory.inter_no==inter_no] + self.rhist = self.rhist.drop_duplicates(subset=['start_unix']).reset_index(drop=True) + + # D_n 및 S_n 값 정의 + self.rhist['D_n'] = 0 # D_n : 시간차이 + self.rhist['S_n'] = 0 # S_n : 현시시간합 + for n in range(len(self.rhist)): + curr_unix = self.rhist.iloc[n].start_unix # current start_unix + self.rhist.loc[n, ['D_n', 'S_n']] = self.calculate_DS(self.rhist, curr_unix) + + # 이전시각, 현재시각 + prev_unix = self.rhist.loc[0, 'start_unix'] # previous start_unix + curr_unix = self.rhist.loc[1, 'start_unix'] # current start_unix + + # rhist의 마지막 행에 도달할 때까지 반복 + while True: + n = self.rhist[self.rhist.start_unix==curr_unix].index[0] + cycle = self.rhist.loc[n, 'cycle'] + D_n = self.rhist.loc[n, 'D_n'] + S_n = self.rhist.loc[n, 'S_n'] + # 참값인 경우 + if (abs(D_n - S_n) <= 5): + pass + # 참값이 아닌 경우 + else: + # 2-1-1. 결측치 처리 : 인접한 두 start_unix의 차이가 계획된 주기의 두 배보다 크면 결측이 일어났다고 판단, 신호계획의 현시시간으로 "대체" + if curr_unix - prev_unix >= 2 * cycle: + # prev_unix를 계획된 주기만큼 늘려가면서 한 행씩 채워나간다. + # (curr_unix와의 차이가 계획된 주기보다 작거나 같아질 때까지) + while curr_unix - prev_unix > cycle: + prev_unix += cycle + # 신호 계획(prow) 불러오기 + start_seconds = np.array(self.timetable.start_seconds) + idx = (start_seconds <= prev_unix).sum() - 1 + start_hour = self.timetable.iloc[idx].start_hour + start_minute = self.timetable.iloc[idx].start_minute + prow = self.plan.copy()[(self.plan.inter_no==inter_no) & (self.plan.start_hour==start_hour) & (self.plan.start_minute==start_minute)] # planned row + # prow에서 필요한 부분을 rhist에 추가 + prow['start_unix'] = prev_unix + prow = prow.drop(['start_hour', 'start_minute', 'offset'], axis=1) + cycle = prow.iloc[0].cycle + self.rhist = pd.concat([self.rhist, prow]) + self.rhist = self.rhist.sort_values(by='start_unix').reset_index(drop=True) + n += 1 + + # 2-1-2. 이상치 처리 : 비율에 따라 해당 행을 "삭제"(R_n <= 0.5) 또는 "조정"(R_n > 0.5)한다 + R_n = (curr_unix - prev_unix) / cycle # R_n : 비율 + # R_n이 0.5보다 작거나 같으면 해당 행을 삭제 + if R_n <= 0.5: + self.rhist = self.rhist.drop(index=n).reset_index(drop=True) + if n >= self.rhist.index[-1]: + break + # 행삭제에 따른 curr_unix, R_n 재정의 + curr_unix = self.rhist.loc[n, 'start_unix'] + R_n = (curr_unix - prev_unix) / cycle # R_n : 비율 + + # R_n이 0.5보다 크면 해당 행 조정 (비율을 유지한 채로 현시시간 대체) + if R_n > 0.5: + # 신호 계획(prow) 불러오기 + start_seconds = np.array(self.timetable.start_seconds) + idx = (start_seconds <= curr_unix).sum() - 1 + start_hour = self.timetable.iloc[idx].start_hour + start_minute = self.timetable.iloc[idx].start_minute + prow = self.plan[(self.plan.inter_no==inter_no) & (self.plan.start_hour==start_hour) & (self.plan.start_minute==start_minute)] # planned row + # 조정된 현시시간 (prow에 R_n을 곱하고 정수로 바꿈) + adjusted_dur = prow.copy()[[f'dura_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] * R_n + int_parts = adjusted_dur.iloc[0].apply(lambda x: int(x)) + frac_parts = adjusted_dur.iloc[0] - int_parts + difference = round(adjusted_dur.iloc[0].sum()) - int_parts.sum() + for _ in range(difference): # 소수 부분이 가장 큰 상위 'difference'개의 값에 대해 올림 처리 + max_frac_index = frac_parts.idxmax() + int_parts[max_frac_index] += 1 + frac_parts[max_frac_index] = 0 # 이미 처리된 항목은 0으로 설정 + # rhist에 조정된 현시시간을 반영 + self.rhist.loc[n, [f'dura_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] = int_parts.values + self.rhist.loc[n, 'cycle'] = int_parts.sum().sum() // 2 + + if n >= self.rhist.index[-1]: + break + prev_unix = curr_unix + curr_unix = self.rhist.loc[n+1, 'start_unix'] + + # 생략해도 무방할 코드 + self.rhist = self.rhist.reset_index(drop=True) + self.rhist = self.rhist.sort_values(by=['start_unix']) + + # D_n 및 S_n 값 재정의 + for n in range(len(self.rhist)): + curr_unix = self.rhist.iloc[n].start_unix # current start_unix + self.rhist.loc[n, ['D_n', 'S_n']] = self.calculate_DS(self.rhist, curr_unix) + self.rhists.append(self.rhist) + self.rhists = pd.concat(self.rhists).sort_values(by=['start_unix','inter_no']) + self.rhists = self.rhists[self.rhists.start_unix >= self.present_time - 3600] + self.rhists = self.rhists.drop(columns=['D_n', 'S_n']) + + def calculate_DS(self, rhist, curr_unix): + program_starts = np.array(self.timetable.start_seconds) + idx = (program_starts <= self.present_time).sum() - 1 + program_start = program_starts[idx] + if list(self.hours[self.hours <= curr_unix]): + ghour_lt_curr_unix = self.hours[self.hours <= curr_unix].max() # the greatest hour less than or equal to curr_unix + else: + ghour_lt_curr_unix = program_start + start_unixes = rhist.start_unix.unique() + start_unixes_lt_ghour = np.sort(start_unixes[start_unixes < ghour_lt_curr_unix]) # start unixes less than ghour_lt_curr_unix + # 기준유닉스(base_unix) : curr_unix보다 작은 hour 중에서 가장 큰 값으로부터 다섯 번째로 작은 start_unix + if len(start_unixes_lt_ghour) > 5: + base_unix = start_unixes_lt_ghour[-5] + # start_unixes_lt_ghour의 길이가 5 미만일 경우에는 맨 앞 start_unix로 base_unix를 지정 + else: + base_unix = rhist.start_unix.min() + D_n = curr_unix - base_unix + S_n_durs = rhist[(rhist.start_unix > base_unix) & (rhist.start_unix <= curr_unix)] \ + [[f'dura_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] + S_n = S_n_durs.values.sum() // 2 + return D_n, S_n + + # 2-2. hrhists + def make_hrhists(self): + # 계층화된 형태로 변환 + self.hrhists = [] # hierarchied recent history + for i, row in self.rhists.iterrows(): + inter_no = row.inter_no + start_unix = row.start_unix + + ind = (self.timetable['start_seconds'] <= row.start_unix).sum() - 1 + start_hour = self.timetable.iloc[ind].start_hour + start_minute = self.timetable.iloc[ind].start_minute + self.isplit = self.isplits[(inter_no, start_hour, start_minute)] + phas_As = [self.isplit[j][0] for j in self.isplit.keys()] + phas_Bs = [self.isplit[j][1] for j in self.isplit.keys()] + durs_A = row[[f'dura_A{j}' for j in range(1,9)]] + durs_B = row[[f'dura_B{j}' for j in range(1,9)]] + durations = [] + for j in range(1, len(self.isplit)+1): + ja = self.isplit[j][0] + jb = self.isplit[j][1] + if ja == jb: + durations.append(min(durs_A[ja-1], durs_B[jb-1])) + else: + durations.append(abs(durs_A[ja-1] - durs_B[ja-1])) + new_rows = pd.DataFrame({'inter_no':[inter_no] * len(durations), 'start_unix':[start_unix] * len(durations), + 'phas_A':phas_As, 'phas_B':phas_Bs, 'duration':durations}) + self.hrhists.append(new_rows) + self.hrhists = pd.concat(self.hrhists) + self.hrhists = self.hrhists.sort_values(by = ['start_unix', 'inter_no', 'phas_A', 'phas_B']).reset_index(drop=True) + + # 3. 이동류정보 전처리 + def process_movement(self): + print("3. 이동류정보 테이블을 변환합니다.") + self.make_movement() + self.update_movement() + + # 3-1. movement + def make_movement(self): + # # - 아래 절차를 5초마다 반복 + # for fsec in range(self.midnight, self.present_time + 1, 5): # fsec : unix time by Five SECond + # # 1. 상태 테이블 조회해서 전체 데이터중 필요데이터(교차로번호, A링 현시번호, A링 이동류번호, B링 현시번호, B링 이동류번호)만 수집 : A + # # move = time2move[fsec] + # move = pd.read_csv(f'../Data/tables/move/move_{fsec}.csv', index_col=0) + # # 2. 이력 테이블 조회해서 교차로별로 유닉스시간 최대인 데이터(교차로변호, 종료유닉스타임)만 수집 : B + # recent_histories = [group.iloc[-1:] for _, group in self.history[self.history['end_unix'] < fsec].groupby('inter_no')] # 교차로별로 유닉스시간이 최대인 행들 + # if not recent_histories: + # rhistory = pd.DataFrame({'inter_no':[], 'end_unix':[]}) # recent history + # else: + # rhistory = pd.concat(recent_histories) + # recent_unix = rhistory[['inter_no', 'end_unix']] + # # 3. 상태 테이블 조회정보(A)와 이력 테이블 조회정보(B) 조인(키값 : 교차로번호) : C + # move = pd.merge(move, recent_unix, how='left', on='inter_no') + # move['end_unix'] = move['end_unix'].fillna(0).astype(int) + # move = move.drop_duplicates() + # # 4. C데이터 프레임에 신규 컬럼(시작 유닉스타임) 생성 후 종료유닉스 타임 값 입력, 종료 유닉스 타임 컬럼 제거 + # move = move.rename(columns = {'end_unix':'start_unix'}) + # # 5. 이동류 이력정보 READ + # # - CSV 파일로 서버에 저장된 이동류정보를 읽어옴(파일이 없는 경우에는 데이터가 없는 프레임 D 생성) + # try: + # if isinstance(self.movement, pd.DataFrame): # movement가 존재할 경우 그걸 그대로 씀. + # pass + # else: + # self.movement = pd.DataFrame() + # except NameError: # movement가 존재하지 않는 경우 생성 + # self.movement = pd.DataFrame() + # # 6. 이동류 이력정보 데이터테이블(D)에 C데이터 add + # self.movement = pd.concat([self.movement, move]) + # # 7. D데이터 프레임에서 중복데이터 제거(교차로번호, 시작 유닉스타임, A링 현시번호, B링 현시번호 같은 행은 제거) + # self.movement = self.movement.drop_duplicates(['inter_no','phas_A','phas_B','start_unix']) + # # 8. D데이터 보관 시간 기준시간을 시작 유닉스 타임의 최대값 - 3600을 값으로 산출하고, 보관 시간 기준시간보다 작은 시작 유닉스 타임을 가진 행은 모두 제거(1시간 데이터만 보관) + # self.movement = self.movement[self.movement.start_unix > fsec - 3600] + # self.movement = self.movement.sort_values(by=['start_unix','inter_no','phas_A','phas_B']).reset_index(drop=True) + self.movement = pd.read_csv(os.path.join(self.path_root, 'Intermediates', 'movement', f'movement_{self.present_time}.csv'), index_col=0) + + # 3-2. movement_updated + def update_movement(self): + # 중복을 제거하고 (inter_no, start_unix) 쌍을 만듭니다. + hrhists_inter_unix = set(self.hrhists[['inter_no', 'start_unix']].drop_duplicates().itertuples(index=False, name=None)) + movement_inter_unix = set(self.movement[['inter_no', 'start_unix']].drop_duplicates().itertuples(index=False, name=None)) + + # hrhists에는 있지만 movement에는 없는 (inter_no, start_unix) 쌍을 찾습니다. + missing_in_movement = hrhists_inter_unix - movement_inter_unix + + # 새로운 행들을 생성합니다. + new_rows = [] + if missing_in_movement: + for inter_no, start_unix in missing_in_movement: + # movements에서 해당 inter_no의 데이터를 찾습니다. + new_row = self.movements[self.movements['inter_no'] == inter_no].copy() + # start_unix 값을 설정합니다. + new_row['start_unix'] = start_unix + new_rows.append(new_row) + + # 새로운 데이터프레임을 생성하고 기존 movement 데이터프레임과 합칩니다. + new_movement = pd.concat(new_rows, ignore_index=True) + self.movement_updated = pd.concat([self.movement, new_movement], ignore_index=True) + else: + self.movement_updated = self.movement + + # 4. 통합테이블 생성 + def make_histids(self): + print("4. 통합 테이블을 생성합니다.") + self.merge_dfs() + self.attach_children() + + # 4-1. histid + def merge_dfs(self): + # movements and durations + movedur = pd.merge(self.hrhists, self.movement_updated, how='inner', on=['inter_no', 'start_unix', 'phas_A', 'phas_B']) + movedur = movedur.sort_values(by=['start_unix', 'inter_no', 'phas_A','phas_B']) + movedur = movedur[['inter_no', 'start_unix', 'phas_A', 'phas_B', 'move_A', 'move_B', 'duration']] + + # 이동류 매칭 테이블에서 진입id, 진출id를 가져와서 붙임. + for i, row in movedur.iterrows(): + inter_no = row.inter_no + start_unix = row.start_unix + # incoming and outgoing edges A + move_A = row.move_A + if move_A in [17, 18]: + inc_edge_A = np.nan + outhedge_A = np.nan + else: + match_A = self.matching[(self.matching.inter_no == inter_no) & (self.matching.move_no == move_A)].iloc[0] + inc_edge_A = match_A.inc_edge + out_edge_A = match_A.out_edge + movedur.loc[i, ['inc_edge_A', 'out_edge_A']] = [inc_edge_A, out_edge_A] + # incoming and outgoing edges B + move_B = row.move_B + if move_B in [17, 18]: + inc_edge_B = np.nan + out_edge_B = np.nan + else: + match_B = self.matching[(self.matching.inter_no == inter_no) & (self.matching.move_no == move_B)].iloc[0] + inc_edge_B = match_B.inc_edge + out_edge_B = match_B.out_edge + movedur.loc[i, ['inc_edge_B', 'out_edge_B']] = [inc_edge_B, out_edge_B] + + # 이동류 컬럼 제거 + movedur = movedur.drop(['move_A', 'move_B'], axis=1) + + self.histid = movedur.copy() # history with edge ids (incoming and outgoing edge ids) + self.histid['node_id'] = self.histid['inter_no'].map(self.inter2node) + self.histid = self.histid[['inter_no', 'node_id', 'start_unix', 'phas_A', 'phas_B', 'duration', 'inc_edge_A', 'out_edge_A', 'inc_edge_B', 'out_edge_B']] + histid_start = self.present_time - 600 + self.histid = self.histid[self.histid.start_unix > histid_start] + + # 4-2. histids + def attach_children(self): + ''' + 자식교차로에 대한 진입·진출 엣지 정보를 붙여주는 함수 + + input : + (1) histid + - 각 교차로에 대한 (시작유닉스, A현시, B현시)별 현시시간, 진입·진출엣지 + - 부모교차로(주교차로)에 대해서만 값이 지정되어 있음 + (2) match6 + - (현시, 링)별 진입·진출엣지 + - 자식교차로(유턴 및 연동교차로)에 대해서도 값이 지정되어 있음 + (3) parent_ids : 부모교차로 목록 + (4) pa2ch : 각 부모교차로id를 부모교차로가 포함하고 있는 자식교차로들의 id들의 리스트로 대응시키는 딕셔너리 + + output : histids + - 모든(부모 및 자식) 교차로에 대한 시작유닉스 (시작유닉스, A현시, B현시)별 현시시간, 진입·진출엣지 + ''' + new_histids = [] + for parent_id in self.parent_ids: + for child_id in self.pa2ch[parent_id]: + new_histid = self.histid.copy()[self.histid.node_id==parent_id] + new_histid[['inc_edge_A', 'out_edge_A', 'inc_edge_B', 'out_edge_B']] = np.nan + for i, row in new_histid.iterrows(): + phas_A = row.phas_A + phas_B = row.phas_B + new_match = self.match6[self.match6.node_id==child_id] + Arow = new_match[(new_match.phase_no==phas_A) & (new_match.ring_type=='A')] + if ~ Arow[['inc_edge', 'out_edge']].isna().all().all(): + inc_edge = Arow.iloc[0].inc_edge + out_edge = Arow.iloc[0].out_edge + new_histid.loc[i, ['inc_edge_A', 'out_edge_A']] = [inc_edge, out_edge] + Brow = new_match[(new_match.phase_no==phas_B) & (new_match.ring_type=='B')] + if ~ Brow[['inc_edge', 'out_edge']].isna().all().all(): + inc_edge = Brow.iloc[0].inc_edge + out_edge = Brow.iloc[0].out_edge + new_histid.loc[i, ['inc_edge_B', 'out_edge_B']] = [inc_edge, out_edge] + new_histid.loc[i, 'node_id'] = child_id + new_histids.append(new_histid) + new_histids = pd.concat(new_histids) + self.histids = pd.concat([self.histid.copy(), new_histids]) + self.histids = self.histids.sort_values(by=['start_unix', 'node_id', 'phas_A', 'phas_B']).reset_index(drop=True) + + # 5. 신호 생성 + print("5. 신호를 생성합니다.") + def get_signals(self): + self.initialize_states() + self.assign_signals() + self.set_timepoints() + self.assign_red_yellow() + self.make_tl_file() + + # 5-1. 신호초기화 + def initialize_states(self): + ''' + 신호 초기화 + + input : + (1) net : 네트워크 + (2) nodes : 노드 목록 + (3) histids : 모든 교차로에 대한 시작유닉스 (시작유닉스, A현시, B현시)별 현시시간, 진입·진출엣지 + + output : node2init + - 각 노드를 초기화된 신호로 맵핑하는 딕셔너리 + - 초기화된 신호란, 우회전을 g로 나머지는 r로 지정한 신호를 말함. + ''' + self.node2init = {} + for node in self.nodes: + node_id = node.getID() + conns = [(c.getJunctionIndex(), c) for c in node.getConnections()] + conns = [c for c in conns if c[0] >= 0] + conns = sorted(conns, key=lambda x: x[0]) + state = [] + for i, ci in conns: + if ci.getTLLinkIndex() < 0: + continue + are_foes = False + for j, cj in conns: + if ci.getTo() == cj.getTo(): + continue + if node.areFoes(i, j): + are_foes = True + break + state.append('r' if are_foes else 'g') + self.node2init[node_id] = state + + # 어떤 연결과도 상충이 일어나지는 않지만, 신호가 부여되어 있는 경우에는 r을 부여 + for _, row in self.histids.iterrows(): + node_id = row['node_id'] + inc_edge_A = row.inc_edge_A + inc_edge_B = row.inc_edge_B + out_edge_A = row.out_edge_A + out_edge_B = row.out_edge_B + + if pd.isna(inc_edge_A) or pd.isna(out_edge_A): + pass + else: + inc_edge_A = self.net.getEdge(inc_edge_A) + out_edge_A = self.net.getEdge(out_edge_A) + for conn in inc_edge_A.getConnections(out_edge_A): + index = conn.getTLLinkIndex() + if index >= 0: + self.node2init[node_id][index] = 'r' + + if pd.isna(inc_edge_B) or pd.isna(out_edge_B): + pass + else: + inc_edge_B = self.net.getEdge(inc_edge_B) + out_edge_B = self.net.getEdge(out_edge_B) + for conn in inc_edge_B.getConnections(out_edge_B): + index = conn.getTLLinkIndex() + if index >= 0: + self.node2init[node_id][index] = 'r' + + # 5-2. 녹색신호 부여 + def assign_signals(self): + ''' + 진입·진출엣지를 신호문자열로 배정 + + input : + (1) histids : 모든 교차로에 대한 (시작유닉스, A현시, B현시)별 현시시간, 진입·진출엣지 + (2) node2init : 각 노드를 초기화된 신호로 맵핑하는 딕셔너리 + (3) net : 네트워크 + + output : sigtable + - 모든 교차로에 대한 (시작유닉스, A현시, B현시)별 현시시간, 신호문자열 + - 황색 및 적색신호는 아직 반영되지 않았음. + ''' + self.sigtable = self.histids.copy() + self.sigtable['init_state'] = self.sigtable['node_id'].map(self.node2init) + self.sigtable['state'] = self.sigtable['init_state'].map(lambda x:''.join(x)) + for i, row in self.sigtable.iterrows(): + node_id = row.node_id + inc_edge_A = row.inc_edge_A + inc_edge_B = row.inc_edge_B + out_edge_A = row.out_edge_A + out_edge_B = row.out_edge_B + state = copy.deepcopy(self.node2init)[node_id] + if pd.isna(inc_edge_A) or pd.isna(out_edge_A): + pass + else: + inc_edge_A = self.net.getEdge(inc_edge_A) + out_edge_A = self.net.getEdge(out_edge_A) + for conn in inc_edge_A.getConnections(out_edge_A): + index = conn.getTLLinkIndex() + if index >= 0: + state[index] = 'G' + self.sigtable.at[i, 'state'] = ''.join(state) + + if pd.isna(inc_edge_B) or pd.isna(out_edge_B): + pass + else: + inc_edge_B = self.net.getEdge(inc_edge_B) + out_edge_B = self.net.getEdge(out_edge_B) + for conn in inc_edge_B.getConnections(out_edge_B): + index = conn.getTLLinkIndex() + if index >= 0: + state[index] = 'G' + self.sigtable.at[i, 'state'] = ''.join(state) + self.sigtable = self.sigtable.dropna(subset='state') + self.sigtable = self.sigtable.reset_index(drop=True) + self.sigtable['phase_sumo'] = self.sigtable.groupby(['node_id', 'start_unix']).cumcount() + self.sigtable = self.sigtable[['node_id', 'start_unix', 'phase_sumo', 'duration', 'state']] + self.sigtable = self.sigtable.sort_values(by=['start_unix', 'node_id']) + self.sigtable['start_dt'] = self.sigtable['start_unix'].apply(lambda x:datetime.fromtimestamp(x)) + + # 5-3. 신호 파일의 시작 및 종료시각 설정 + def set_timepoints(self): + self.offsets = {} + self.Sigtable = [] + sim_start = self.present_time - 300 + for node_id, group in self.sigtable.groupby('node_id'): + lsbs = group[group['start_unix'] < sim_start]['start_unix'].max() # the last start_unix before sim_start + self.offsets[node_id] = lsbs - sim_start + group = group[group.start_unix >= lsbs] + start_unixes = np.array(group.start_unix) + start_unixes = np.sort(np.unique(start_unixes))[:self.node2num_cycles[node_id]] + group = group[group.start_unix.isin(start_unixes)] + self.Sigtable.append(group) + self.Sigtable = pd.concat(self.Sigtable) + + # 5-4. 적색 및 황색신호 부여 + def assign_red_yellow(self): + ''' + 적색, 황색신호를 반영한 신호문자열 배정 + + input : Sigtable + - 모든 교차로에 대한 (시작유닉스, 세부현시번호)별 현시시간, 신호문자열, 진입·진출엣지 + * 세부현시란 오버랩을 반영한 현시번호를 뜻함. + + output : SIGTABLE + - 모든 교차로에 대한 (시작유닉스, 녹황적세부현시번호)별 현시시간, (황·적색신호가 포함된) 신호문자열 + * 녹황적세부현시번호란 세부현시번호에 r, g, y 옵션까지 포함된 현시번호를 뜻함. + ''' + self.SIGTABLE = [] + for node_id, group in self.Sigtable.groupby('node_id'): + new_rows_list = [] + for i in range(1, len(group)): + prev_row = group.iloc[i-1:i].copy() + next_row = group.iloc[i:i+1].copy() + new_rows = pd.concat([prev_row, prev_row, next_row]).reset_index(drop=True) + new_rows.loc[0, 'phase_sumo'] = str(prev_row.phase_sumo.iloc[0]) + '_g' + new_rows.loc[0, 'duration'] = new_rows.loc[0, 'duration'] - 5 + new_rows.loc[1, 'phase_sumo'] = str(prev_row.phase_sumo.iloc[0]) + '_y' + new_rows.loc[1, 'duration'] = 4 + yellow_state = '' + red_state = '' + for a, b in zip(prev_row.state.iloc[0], next_row.state.iloc[0]): + if a == 'G' and b == 'r': + yellow_state += 'y' + red_state += 'r' + else: + yellow_state += a + red_state += a + new_rows.loc[2, 'phase_sumo'] = str(next_row.phase_sumo.iloc[0]) + '__r' + new_rows.loc[2, 'duration'] = 1 + new_rows.loc[1, 'state'] = yellow_state + new_rows.loc[2, 'state'] = red_state + new_rows_list.append(new_rows) + next_row['phase_sumo'] = str(next_row.phase_sumo.iloc[0]) + '_g' + next_row['duration'] -= 5 + # next_row.loc['duration'] -= 5 + new_rows_list.append(next_row) + new_rows = pd.concat(new_rows_list) + self.SIGTABLE.append(new_rows) + self.SIGTABLE = pd.concat(self.SIGTABLE).sort_values(by=['node_id', 'start_unix', 'phase_sumo']).reset_index(drop=True) + + # 5-5. 신호파일 생성 + def make_tl_file(self): + strings = ['\n'] + for node_id, group in self.SIGTABLE.groupby('node_id'): + strings.append(f' \n') + for i, row in group.iterrows(): + duration = row.duration + state = row.state + strings.append(f' \n') + strings.append(' \n') + strings.append('') + strings = ''.join(strings) + # 저장 + self.path_output = os.path.join(self.path_root, 'Results', f'sn_{self.present_time}.add.xml') + with open(self.path_output, 'w') as f: + f.write(strings) + + # 6. 이슈사항 저장 + def write_issues(self): + print('6. 이슈사항을 저장합니다.') + path_issues = os.path.join(self.path_root, "Results", "issues_generate_signals.txt") + with open(path_issues, "w", encoding="utf-8") as file: + for item in self.issues: + file.write(item + "\n") + if self.issues: + print("데이터 처리 중 발생한 특이사항은 다음과 같습니다. :") + for review in self.issues: + print(review) + + def main(self): + # 1. 데이터 준비 + self.prepare_data() + # 2. 신호이력 전처리 + self.process_history() + # 3. 이동류정보 전처리 + self.process_movement() + # 4. 통합테이블 생성 + self.make_histids() + # 5. 신호 생성 + self.get_signals() + # 6. 이슈사항 저장 + self.write_issues() + +if __name__ == '__main__': + self = SignalGenerator() + self.main() + # self.histid.to_csv(os.path.join('Intermediates', 'histid', f'histid_{self.present_time}_.csv')) \ No newline at end of file diff --git a/Script/get_intermediates.py b/Script/get_intermediates.py deleted file mode 100644 index 6d5cf56a8..000000000 --- a/Script/get_intermediates.py +++ /dev/null @@ -1,587 +0,0 @@ -import pandas as pd -import numpy as np -import os, sys -import json -import sumolib, traci -from tqdm import tqdm - -class DailyPreprocess(): - def __init__(self): - self.path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - self.issues = [] - - # 1. 데이터 불러오기 - def load_data(self): - self.load_networks() - self.load_tables() - self.check_networks() - self.check_tables() - print('1. 모든 데이터가 로드되었습니다.') - - # 1-1. 네트워크 불러오기 - def load_networks(self): - self.net = sumolib.net.readNet(os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')) - print("1-1. 네트워크가 로드되었습니다.") - - # 1-2. 테이블 불러오기 - def load_tables(self): - # 모든 컬럼에 대하여 데이터타입 지정 - loading_dtype = { - 'inter_no':'int', 'start_hour':'int', 'start_minute':'int', 'cycle':'int','offset':'int', - 'node_id':'str', 'inter_type':'str', 'parent_id':'str','child_id':'str', - 'direction':'str', 'condition':'str', 'inc_edge':'str', 'out_edge':'str', - 'end_unix':'int', 'inter_name':'str', 'inter_lat':'float', 'inter_lon':'float', - 'group_no':'int', 'main_phase_no':'int', 'phase_no':'int','ring_type':'str' - } - for alph in ['A', 'B']: - for j in range(1,9): - loading_dtype[f'angle_{alph}{j}'] = 'str' - loading_dtype[f'dura_{alph}{j}'] = 'int' - - self.path_table = os.path.join(self.path_root, 'Data', 'tables') - - self.inter_info = pd.read_csv(os.path.join(self.path_table, 'inter_info.csv'), dtype=loading_dtype) - self.angle = pd.read_csv(os.path.join(self.path_table, 'angle.csv'), dtype=loading_dtype) - self.plan = pd.read_csv(os.path.join(self.path_table, 'plan.csv'), dtype=loading_dtype) - self.inter_node = pd.read_csv(os.path.join(self.path_table, 'inter_node.csv'), dtype=loading_dtype) - self.uturn = pd.read_csv(os.path.join(self.path_table, 'child_uturn.csv'), dtype=loading_dtype) - self.coord = pd.read_csv(os.path.join(self.path_table, 'child_coord.csv'), dtype=loading_dtype) - self.nema = pd.read_csv(os.path.join(self.path_table, 'nema.csv'), encoding='cp949', dtype=loading_dtype) - print("1-2. 테이블들이 로드되었습니다.") - - # 1-3. 테이블 불러오기 - def check_networks(self): - # https://sumo.dlr.de/docs/Netedit/neteditUsageExamples.html#simplify_tls_program_state_after_changing_connections - if 'SUMO_HOME' in os.environ: - tools = os.path.join(os.environ['SUMO_HOME'], 'tools') - if tools not in sys.path: - sys.path.append(tools) - else: - raise EnvironmentError("please declare environment variable 'SUMO_HOME'") - traci.start([sumolib.checkBinary('sumo'), "-n", os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')]) - nodes = [node for node in self.net.getNodes() if node.getType()=='traffic_light'] - for node in nodes: - node_id = node.getID() - from_xml = len([c for c in node.getConnections() if c.getTLLinkIndex() >= 0]) - from_traci = len(traci.trafficlight.getRedYellowGreenState(node_id)) - if from_xml != from_traci: - sub = {'id': node_id, 'type': 'node', 'note': '유효하지 않은 연결이있음. netedit에서 clean states 필요.'} - self.issues.append(sub) - traci.close() - print("1-3. 네트워크의 모든 clean state requirement들을 체크했습니다.") - - # 1-4. 테이블의 무결성 검사 - def check_tables(self): - self.check_inter_info() - self.check_angle() - self.check_plan() - print("1-4. 모든 테이블들의 무결성을 검사했고 이상 없습니다.") - pass - - # 1-4-1. 교차로정보(inter_info) 검사 - def check_inter_info(self): - # 1-4-1-1. inter_lat, inter_lon 적절성 검사 - # self.inter_info.loc[0, 'inter_lat'] = 38.0 # 에러 발생을 위한 코드 - self.max_lon, self.min_lon = 127.207888, 127.012492 - self.max_lat, self.min_lat = 37.480693, 37.337112 - for _, row in self.inter_info.iterrows(): - latbool = self.min_lat <= row['inter_lat'] <= self.max_lat - lonbool = self.min_lon <= row['inter_lon'] <= self.max_lon - if not(latbool and lonbool): - msg = f"1-4-1-1. 위도 또는 경도가 범위를 벗어난 교차로가 있습니다: inter_no : {row['inter_no']}" - self.issues.append(msg) - # 교차로목록 정의 - self.inter_nos = sorted(self.inter_info.inter_no.unique()) - - # 1-4-2. 방위각정보(inter_info) 검사 - def check_angle(self): - # 1-4-2-1. inter_no 검사 - # self.angle.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드 - missing_inter_nos = set(self.angle.inter_no) - set(self.inter_nos) - if missing_inter_nos: - msg = f"1-4-2-1. angle의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}" - self.issues.append(msg) - - # 1-4-3. 신호계획(plan) 검사 - def check_plan(self): - # 1-4-3-1. inter_no 검사 - # self.plan.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드 - missing_inter_nos = set(self.plan.inter_no) - set(self.inter_nos) - if missing_inter_nos: - msg = f"1-4-3-1. plan의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}" - self.issues.append(msg) - - # 1-4-3-2. 시작시각 검사 - # self.plan.loc[0, 'start_hour'] = 27 # 에러 발생을 위한 코드 - for _, row in self.plan.iterrows(): - start_hour = row.start_hour - start_minute = row.start_minute - if not (0 <= start_hour <= 23) or not (0 <= start_minute <= 59): - msg = f"1-4-3-2. plan에 잘못된 형식의 start_time이 존재합니다: {start_hour, start_minute}" - self.issues.append(msg) - - # 1-4-3-3. 현시시간 검사 - # self.plan.loc[0, 'dura_A1'] = -2 # 에러 발생을 위한 코드 - durations = self.plan[[f'dura_{alph}{j}' for alph in ['A','B'] for j in range(1, 9)]] - valid_indices = ((durations >= 0) & (durations <= 200)).all(axis=1) - invalid_inter_nos = sorted(self.plan[~ valid_indices].inter_no.unique()) - if invalid_inter_nos: - msg = f"1-4-3-3. 음수이거나 200보다 큰 현시시간이 존재합니다. : {invalid_inter_nos}" - - # 1-4-3-4. 주기 일관성 검사 - # self.plan.loc[0, 'cycle'] = 50 # 에러 발생을 위한 코드 - inconsistent_cycle = self.plan.groupby(['inter_no', 'start_hour', 'start_minute'])['cycle'].nunique().gt(1) - if inconsistent_cycle.any(): - inc_inter_no, start_hour, start_minute = inconsistent_cycle[inconsistent_cycle].index[0] - msg = f"1-4-3-4. inter_no:{inc_inter_no}, start_hour:{start_minute}, start_hour:{start_minute}일 때, cycle이 유일하게 결정되지 않습니다." - self.issues.append(msg) - - # 1-4-3-5. 현시시간 / 주기 검사 - # self.plan.loc[0, 'duration'] = 10 # 에러 발생을 위한 코드 - right_duration = True - for (inter_no, start_hour, start_minute), group in self.plan.groupby(['inter_no', 'start_hour', 'start_minute']): - A_sum = group[[f'dura_A{j}' for j in range(1, 9)]].iloc[0].sum() - B_sum = group[[f'dura_B{j}' for j in range(1, 9)]].iloc[0].sum() - # A_sum = group[group['ring_type']=='A']['duration'].sum() - # B_sum = group[group['ring_type']=='B']['duration'].sum() - cycle = group['cycle'].unique()[0] - if not (A_sum == B_sum == cycle): - right_duration = False - inc_inter_no = inter_no - if not right_duration: - msg = f"1-4-4-5. inter_no:{inc_inter_no}, A링현시시간의 합과 B링현시시간의 합이 일치하지 않거나, 현시시간의 합과 주기가 일치하지 않습니다." - self.issues.append(msg) - - # 2. 중간산출물 만들기 - def get_intermediates(self): - self.get_matches() - # self.get_movements() - self.get_node2num_cycles() - - # 2-1 매칭테이블들 생성 - def get_matches(self): - self.make_match1() - self.make_match2() - self.make_match3() - self.make_match4() - self.make_match5() - self.make_match6() - self.make_matching() - - # 2-1-1 - def make_match1(self): - ''' - 신호 DB에는 매 초마다 이동류정보가 업데이트 된다. 그리고 이 이동류정보를 매 5초마다 불러와서 사용하게 된다. - '../Data/tables/move/'에는 5초마다의 이동류정보가 저장되어 있다. - - return : 통합된 이동류정보 - - 모든 inter_no(교차로번호)에 대한 A, B링 현시별 이동류정보 - - match1을 만드는 데 시간이 소요되므로 한 번 만들어서 저장해두고 저장해둔 것을 쓴다. - ''' - # [이동류번호] 불러오기 (약 1분의 소요시간) - path_move = os.path.join(self.path_root, 'Data', 'tables', 'move') - csv_moves = os.listdir(path_move) - moves = [pd.read_csv(os.path.join(path_move, csv_move), index_col=0) for csv_move in tqdm(csv_moves, desc='이동류정보 불러오는 중 : match1')] - self.match1 = pd.concat(moves).drop_duplicates().sort_values(by=['inter_no','phas_A','phas_B']).reset_index(drop=True) - self.match1.to_csv(os.path.join(self.path_root, 'Intermediates', 'match1.csv')) - - # 2-1-2 - def make_match2(self): - ''' - match1을 계층화함. - - match1의 컬럼 : inter_no, phas_A, phas_B, move_A, move_B - - match2의 컬럼 : inter_no, phase_no, ring_type, move_no - ''' - # 계층화 (inter_no, phas_A, phas_B, move_A, move_B) -> ('inter_no', 'phase_no', 'ring_type', 'move_no') - matchA = self.match1[['inter_no', 'phas_A', 'move_A']].copy() - matchA.columns = ['inter_no', 'phase_no', 'move_no'] - matchA['ring_type'] = 'A' - matchB = self.match1[['inter_no', 'phas_B', 'move_B']].copy() - matchB.columns = ['inter_no', 'phase_no', 'move_no'] - matchB['ring_type'] = 'B' - self.match2 = pd.concat([matchA, matchB]).drop_duplicates() - self.match2 = self.match2[['inter_no', 'phase_no', 'ring_type', 'move_no']] - self.match2 = self.match2.sort_values(by=list(self.match2.columns)) - - # 2-1-3 - def make_match3(self): - ''' - 각 movement들에 방향(진입방향, 진출방향)을 매칭시켜 추가함. - - match2의 컬럼 : inter_no, phase_no, ring_type, move_no - - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir - - nema : - - 컬럼 : move_no, inc_dir, out_dir - - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 - - 이동류번호 : 1 ~ 16, 17, 18, 21 - - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 - ''' - # nema 정보 불러오기 및 병합 - self.match3 = pd.merge(self.match2, self.nema, how='left', on='move_no').drop_duplicates() - - # 2-1-4 - def make_match4(self): - ''' - 방위각 정보를 매칭시켜 추가함. - - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir - - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle - - angle_original : - - 컬럼 : inter_no, angle_Aj, angle_Bj (j : 1 ~ 8) - - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 - - 이동류번호 : 1 ~ 16, 17, 18, 21 - - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 - ''' - - # 계층화 - angles = [] - for i, row in self.angle.iterrows(): - angle_codes = row[[f'angle_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] - new = pd.DataFrame({'inter_no':[row.inter_no] * 16, 'phase_no':list(range(1, 9))*2, 'ring_type':['A'] * 8 + ['B'] * 8, 'angle_code':angle_codes.to_list()}) - angles.append(new) - angles = pd.concat(angles) - angles = angles.dropna().reset_index(drop=True) - - # 병합 - six_chars = angles.angle_code.apply(lambda x:len(x)==6) - angles.loc[six_chars,'inc_angle'] = angles.angle_code.apply(lambda x:x[:3]) - angles.loc[six_chars,'out_angle'] = angles.angle_code.apply(lambda x:x[3:]) - angles = angles.drop('angle_code', axis=1) - self.match4 = pd.merge(self.match3, angles, how='left', left_on=['inter_no', 'phase_no', 'ring_type'], - right_on=['inter_no', 'phase_no', 'ring_type']).drop_duplicates() - - # 2-1-5 - def make_match5(self): - ''' - 진입엣지id, 진출엣지id, 노드id를 추가함 (주교차로). - - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle - - match5의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id - - 사용된 데이터 : - (1) net - - 성남시 정자동 부근의 샘플 네트워크 - (2) inter_node - - 교차로번호와 노드id를 매칭시키는 테이블. - - parent/child 정보도 포함되어 있음 - - 컬럼 : inter_no, node_id, inter_type - (3) inter_info - - 교차로 정보. 여기에서는 위도와 경도가 쓰임. - - 컬럼 : inter_no, inter_name, inter_lat, inter_lon, group_no, main_phase_no - - 진입엣지id, 진출엣지id를 얻는 과정 : - - match5 = match4.copy()의 각 열을 순회하면서 아래 과정을 반복함. - * 진입에 대해서만 서술하겠지만 진출도 마찬가지로 설명될 수 있음 - - 해당 행의 교차로정보로부터 노드ID를 얻어내고, 해당 노드에 대한 모든 진출엣지id를 inc_edges에 저장. - * inc_edge(진입엣지) : incoming edge, out_edge(진출엣지) : outgoing_edge - - inc_edges의 모든 진입엣지에 대하여 진입방향(inc_dires, 2차원 단위벡터)을 얻어냄. - - 해당 행의 진입각으로부터 그에 대응되는 진입각방향(단위벡터)를 얻어냄. - - 주어진 진입각방향에 대하여 내적이 가장 작은 진입방향에 대한 진입엣지를 inc_edge_id로 지정함. - ''' - - # parent node만 가져옴. - inter_node1 = self.inter_node[self.inter_node.inter_type == 'parent'].drop('inter_type', axis=1) - inter_info1 = self.inter_info[['inter_no', 'inter_lat', 'inter_lon']] - inter = pd.merge(inter_node1, inter_info1, how='left', left_on=['inter_no'], - right_on=['inter_no']).drop_duplicates() - - self.inter2node = dict(zip(inter['inter_no'], inter['node_id'])) - - self.match5 = self.match4.copy() - # 진입진출ID 매칭 - for index, row in self.match5.iterrows(): - node_id = self.inter2node[row.inter_no] - node = self.net.getNode(node_id) - # 교차로의 모든 (from / to) edges - inc_edges = [edge for edge in node.getIncoming() if edge.getFunction() == ''] # incoming edges - out_edges = [edge for edge in node.getOutgoing() if edge.getFunction() == ''] # outgoing edges - # 교차로의 모든 (from / to) directions - inc_dirs = [] - for inc_edge in inc_edges: - start = inc_edge.getShape()[-2] - end = inc_edge.getShape()[-1] - inc_dir = np.array(end) - np.array(start) - inc_dir = inc_dir / (inc_dir ** 2).sum() ** 0.5 - inc_dirs.append(inc_dir) - out_dirs = [] - for out_edge in out_edges: - start = out_edge.getShape()[0] - end = out_edge.getShape()[1] - out_dir = np.array(end) - np.array(start) - out_dir = out_dir / (out_dir ** 2).sum() ** 0.5 - out_dirs.append(out_dir) - # 진입각, 진출각 불러오기 - if not pd.isna(row.inc_angle): - inc_angle = int(row.inc_angle) - out_angle = int(row.out_angle) - # 방위각을 일반각으로 가공, 라디안 변환, 단위벡터로 변환 - inc_angle = (-90 - inc_angle) % 360 - inc_angle = inc_angle * np.pi / 180. - inc_dir_true = np.array([np.cos(inc_angle), np.sin(inc_angle)]) - out_angle = (90 - out_angle) % 360 - out_angle = out_angle * np.pi / 180. - out_dir_true = np.array([np.cos(out_angle), np.sin(out_angle)]) - # 매칭 엣지 반환 - inc_index = np.array([np.dot(inc_dir, inc_dir_true) for inc_dir in inc_dirs]).argmax() - out_index = np.array([np.dot(out_dir, out_dir_true) for out_dir in out_dirs]).argmax() - inc_edge_id = inc_edges[inc_index].getID() - out_edge_id = out_edges[out_index].getID() - self.match5.at[index, 'inc_edge'] = inc_edge_id - self.match5.at[index, 'out_edge'] = out_edge_id - self.match5['node_id'] = self.match5['inter_no'].map(self.inter2node) - self.match5 = self.match5.sort_values(by=['inter_no','phase_no','ring_type']).reset_index(drop=True) - - # 2-1-6 - def make_match6(self): - ''' - 진입엣지id, 진출엣지id, 노드id를 추가함 (부교차로). - - match6의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id - - 사용된 데이터 : - (1) inter_node - - 교차로번호와 노드id를 매칭시키는 테이블. - - parent/child 정보도 포함되어 있음 - - 컬럼 : inter_no, node_id, inter_type - (2) uturn (유턴정보) - - 컬럼 : parent_id, child_id, direction, condition, inc_edge, out_edge - - parent_id, child_id : 주교차로id, 유턴교차로id - - direction : 주교차로에 대한 유턴노드의 상대적인 위치(방향) - - condition : 좌회전시, 직진시, 직좌시, 보행신호시 중 하나 - - inc_edge, out_edge : 유턴에 대한 진입진출엣지 - (3) coord (연동교차로정보) - - 컬럼 : parent_id, child_id, phase_no, ring_type, inc_edge, out_edge - - parent_id, child_id : 주교차로id, 연동교차로id - - 나머지 컬럼 : 각 (현시, 링)별 진입진출엣지 - - 설명 : - - match5는 주교차로에 대해서만 진입엣지id, 진출엣지id, 노드id를 추가했었음. - 여기에서 uturn, coord를 사용해서 부교차로들(유턴교차로, 연동교차로)에 대해서도 해당 값들을 부여함. - 유턴교차로 : - - directions를 정북기준 시계방향의 8방위로 정함. - - 이를 통해 진입방향이 주어진 경우에 좌회전, 직진, 보행 등에 대한 (진입방향, 진출방향)을 얻어낼 수 있음. - - 예) 진입방향(direction)이 '북'일 때, - - 직진 : (북, 남) - * 남 : directions[(ind + 4) % len(directions)] - - 좌회전 : (북, 동) - * 동 : directions[(ind + 2) % len(directions)] - - 보행 : (서, 동) - * 서 : directions[(ind - 2) % len(directions)] - - uturn의 각 행을 순회하면서 아래 과정을 반복함 - - match5에서 parent_id에 해당하는 행들을 가져옴(cmatch). - - condition 별로 진입방향, 진출방향A, 진출방향B 정함. - - 상술한 directions를 활용하여 정함. - - (진입방향, 진출방향A, 진출방향B)을 고려하여 (현시, 링) 별로 진입엣지id, 진출엣지id를 정함. - - ex) cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - - 순회하면서 만든 cmatch를 cmatchs라는 리스트에 저장함. - - 연동교차로 : - - 연동교차로의 경우 coord에 (현시, 링)별 진입엣지ID, 진출엣지ID가 명시되어 있음. - - 'inc_dir', 'out_dir', 'inc_angle','out_angle'와 같은 열들은 np.nan을 지정해놓음. - - 이 열들은, 사실상 다음 스텝부터는 사용되지 않는 열들이기 때문에 np.nan으로 지정해놓아도 문제없음. - - match6 : - - 이렇게 얻은 match5, cmatchs, coord를 모두 pd.concat하여 match6을 얻어냄. - ''' - - self.node2inter = dict(zip(self.inter_node['node_id'], self.inter_node['inter_no'])) - - child_ids = self.inter_node[self.inter_node.inter_type=='child'].node_id.unique() - ch2pa = {} # child to parent - for child_id in child_ids: - parent_no = self.inter_node[self.inter_node.node_id==child_id].inter_no.iloc[0] - sub_inter_node = self.inter_node[self.inter_node.inter_no==parent_no] - ch2pa[child_id] = sub_inter_node[sub_inter_node.inter_type=='parent'].iloc[0].node_id - directions = ['북', '북동', '동', '남동', '남', '남서', '서', '북서'] # 정북기준 시계방향으로 8방향 - - # 각 uturn node에 대하여 (inc_edge_id, out_edge_id) 부여 - cmatches = [] - for _, row in self.uturn.iterrows(): - child_id = row.child_id - parent_id = row.parent_id - direction = row.direction - condition = row.condition - inc_edge_id = row.inc_edge - out_edge_id = row.out_edge - # match5에서 parent_id에 해당하는 행들을 가져옴 - cmatch = self.match5.copy()[self.match5.node_id==parent_id] # match dataframe for a child node - cmatch = cmatch.sort_values(by=['phase_no', 'ring_type']).reset_index(drop=True) - cmatch['node_id'] = child_id - cmatch[['inc_edge', 'out_edge']] = np.nan - - # condition 별로 inc_dire, out_dire_A, out_dire_B를 정함 - ind = directions.index(direction) - if condition == "좌회전시": - inc_dire = direction - out_dire_A = out_dire_B = directions[(ind + 2) % len(directions)] - elif condition == "직진시": - inc_dire = direction - out_dire_A = out_dire_B = directions[(ind + 4) % len(directions)] - elif condition == "보행신호시": - inc_dire = directions[(ind + 2) % len(directions)] - out_dire_A = directions[(ind - 2) % len(directions)] - out_dire_B = directions[(ind - 2) % len(directions)] - - # (inc_dire, out_dire_A, out_dire_B) 별로 inc_edge_id, out_edge_id를 정함 - if condition == '보행신호시': - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - # 이동류번호가 17(보행신호)이면서 유턴노드방향으로 가는 신호가 없으면 (inc_edge_id, out_edge_id)를 부여한다. - cmatch.loc[(cmatch.move_no==17) & (cmatch.out_dir!=direction), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - else: # '직진시', '좌회전시' - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - # 유턴신호의 이동류번호를 19로 부여한다. - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), 'move_no'] = 19 - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), 'move_no'] = 19 - cmatches.append(cmatch) - - # 각 coordination node에 대하여 (inc_edge_id, out_edge_id) 부여 - self.coord['inter_no'] = self.coord['parent_id'].map(self.node2inter) - self.coord = self.coord.rename(columns={'child_id':'node_id'}) - self.coord[['inc_dir', 'out_dir', 'inc_angle','out_angle']] = np.nan - self.coord['move_no'] = 20 - self.coord = self.coord[['inter_no', 'phase_no', 'ring_type', 'move_no', 'inc_dir', 'out_dir', 'inc_angle','out_angle', 'inc_edge', 'out_edge', 'node_id']] - - # display(coord) - cmatches = pd.concat(cmatches) - self.match6 = pd.concat([self.match5, cmatches, self.coord]).drop_duplicates().sort_values(by=['inter_no', 'node_id', 'phase_no', 'ring_type']) - self.match6.to_csv(os.path.join(self.path_root, 'Intermediates', 'match6.csv')) - - # 2-1-7 - def make_matching(self): - ''' - 이동류 매칭 : 각 교차로에 대하여, 가능한 모든 이동류 (1~18, 21)에 대한 진입·진출엣지ID를 지정한다. - 모든 이동류에 대해 지정하므로, 시차제시 이전과 다른 이동류가 등장하더라도 항상 진입·진출 엣지 ID를 지정할 수 있다. - - matching의 컬럼 : inter_no, move_no, inc_dir, out_dir, inc_edge, out_edge, node_id - - 설명 : - - 필요한 리스트, 딕셔너리 등을 정의 - (1) 가능한 (진입방향, 진출방향) 목록 [리스트] - (2) 각 교차로별 방향 목록 : pdires (possible directions) [딕셔너리] - (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) [딕셔너리] - (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) [딕셔너리] - (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) [딕셔너리] - - matching은 빈 리스트로 지정. - - 모든 노드id에 대하여 다음 과정을 반복 - - 해당 노드id에 대한 모든 가능한 (진입방향, 진출방향)에 대하여 다음 과정을 반복 - - (노드id, 진입방향)으로부터 진입엣지id를 얻어냄. 마찬가지로 진출엣지id도 얻어냄 - - 얻어낸 정보를 바탕으로 한 행(new_row)을 만들고 이것을 matching에 append - ''' - - self.match7 = self.match6.copy() - self.match7 = self.match7[['inter_no', 'move_no', 'inc_dir', 'out_dir', 'inc_edge', 'out_edge', 'node_id']] - - parent_ids = sorted(self.inter_node[self.inter_node.inter_type=='parent'].node_id.unique()) - child_ids = sorted(self.inter_node[self.inter_node.inter_type=='child'].node_id.unique()) - - # (1) 가능한 (진입방향, 진출방향) 목록 - flows = self.nema.dropna().apply(lambda row: (row['inc_dir'], row['out_dir']), axis=1).tolist() - # (2) 각 교차로별 방향 목록 : pdires (possible directions) - pdires = {} - for node_id in parent_ids: - dires = self.match7[self.match7.node_id == node_id][['inc_dir','out_dir']].values.flatten() - dires = {dire for dire in dires if type(dire)==str} - pdires[node_id] = dires - # (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) - inc2id = {} - for node_id in parent_ids: - for inc_dir in pdires[node_id]: - df = self.match7[(self.match7.node_id==node_id) & (self.match7.inc_dir==inc_dir)] - inc2id[(node_id, inc_dir)] = df.inc_edge.iloc[0] - # (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) - out2id = {} - for node_id in parent_ids: - for out_dir in pdires[node_id]: - df = self.match7[(self.match7.node_id==node_id) & (self.match7.out_dir==out_dir)] - out2id[(node_id, out_dir)] = df.out_edge.iloc[0] - # (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) - pflow = {} - for node_id in parent_ids: - pflow[node_id] = [flow for flow in flows if set(flow).issubset(pdires[node_id])] - # (6) 가능한 이동류에 대하여 진입id, 진출id 배정 : matching - # node2inter = dict(zip(self.match7['node_id'], self.match7['inter_no'])) - dires_right = ['북', '서', '남', '동', '북'] # ex (북, 서), (서, 남) 등은 우회전 flow - self.matching = [] - for node_id in parent_ids: - inter_no = self.node2inter[node_id] - # 좌회전과 직진(1 ~ 16) - for (inc_dir, out_dir) in pflow[node_id]: - move_no = self.nema[(self.nema.inc_dir==inc_dir) & (self.nema.out_dir==out_dir)].move_no.iloc[0] - inc_edge = inc2id[(node_id, inc_dir)] - out_edge = out2id[(node_id, out_dir)] - new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[move_no], - 'inc_dir':[inc_dir], 'out_dir':[out_dir], - 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) - self.matching.append(new_row) - # 보행신호(17), 전적색(18) - new_row = pd.DataFrame({'inter_no':[inter_no] * 2, 'move_no':[17, 18], - 'inc_dir':[None]*2, 'out_dir':[None]*2, - 'inc_edge':[None]*2, 'out_edge':[None]*2, 'node_id':[node_id]*2}) - self.matching.append(new_row) - # 신호우회전(21) - for d in range(len(dires_right)-1): - inc_dir = dires_right[d] - out_dir = dires_right[d+1] - if {inc_dir, out_dir}.issubset(pdires[node_id]): - inc_edge = inc2id[(node_id, inc_dir)] - out_edge = out2id[(node_id, out_dir)] - new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[21], - 'inc_dir':[inc_dir], 'out_dir':[out_dir], - 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) - self.matching.append(new_row) - self.matching.append(self.match7[self.match7.node_id.isin(child_ids)]) - self.matching = pd.concat(self.matching) - self.matching = self.matching.dropna().sort_values(by=['inter_no', 'node_id', 'move_no']).reset_index(drop=True) - self.matching['move_no'] = self.matching['move_no'].astype(int) - self.matching.to_csv(os.path.join(self.path_root, 'Intermediates', 'matching.csv')) - - # 2-2 - def get_movements(self): - movements_path = os.path.join(self.path_root, 'Intermediates', 'movement') - movements_list = [pd.read_csv(os.path.join(movements_path, file), index_col=0) for file in tqdm(os.listdir(movements_path), desc='이동류정보 불러오는 중 : movements')] - movements = pd.concat(movements_list) - movements = movements.drop(columns=['start_unix']) - movements = movements.drop_duplicates() - movements = movements.sort_values(by=['inter_no', 'phas_A', 'phas_B']) - movements = movements.reset_index(drop=True) - movements.to_csv(os.path.join(self.path_root, 'Intermediates', 'movements.csv')) - return movements - - # 2-3 node2num_cycles : A dictionary that maps a node_id to the number of cycles - def get_node2num_cycles(self): - # node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no'])) - self.node_ids = sorted(self.inter_node.node_id.unique()) - - Aplan = self.plan.copy()[['inter_no'] + [f'dura_A{j}' for j in range(1,9)] + ['cycle']] - grouped = Aplan.groupby('inter_no') - df = grouped.agg({'cycle': 'min'}).reset_index() - df = df.rename(columns={'cycle': 'min_cycle'}) - df['num_cycle'] = 300 // df['min_cycle'] + 2 - inter2num_cycles = dict(zip(df['inter_no'], df['num_cycle'])) - node2numcycles = {node_id : inter2num_cycles[self.node2inter[node_id]] for node_id in self.node_ids} - with open(os.path.join('Intermediates','node2numcycles.json'), 'w') as file: - json.dump(node2numcycles, file, indent=4) - return node2numcycles - - # 3. 이슈사항 저장 - def write_issues(self): - path_issues = os.path.join(self.path_root, "Results", "issues_intermediates.txt") - with open(path_issues, "w", encoding="utf-8") as file: - for item in self.issues: - file.write(item + "\n") - if self.issues: - print("데이터 처리 중 발생한 특이사항은 다음과 같습니다. :") - for review in self.issues: - print(review) - - def main(self): - # 1. 데이터 불러오기 - self.load_data() - # 2. 중간산출물 만들기 - self.get_intermediates() - # 3. 이슈사항 저장 - self.write_issues() - -if __name__ == '__main__': - self = DailyPreprocess() - self.main() \ No newline at end of file diff --git a/Script/get_signals.py b/Script/get_signals.py deleted file mode 100644 index df8f1c91f..000000000 --- a/Script/get_signals.py +++ /dev/null @@ -1,587 +0,0 @@ -import pandas as pd -import numpy as np -import os, sys -import json -from tqdm import tqdm -from datetime import datetime - -class SignalGenerator(): - def __init__(self): - self.path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - self.issues = [] - - # 1. 데이터 불러오기 - def load_data(self): - self.load_networks() - self.load_tables() - self.check_networks() - self.check_tables() - print('1. 모든 데이터가 로드되었습니다.') - - # 1-1. 네트워크 불러오기 - def load_networks(self): - self.net = sumolib.net.readNet(os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')) - print("1-1. 네트워크가 로드되었습니다.") - - # 1-2. 테이블 불러오기 - def load_tables(self): - # 모든 컬럼에 대하여 데이터타입 지정 - loading_dtype = { - 'inter_no':'int', 'start_hour':'int', 'start_minute':'int', 'cycle':'int','offset':'int', - 'node_id':'str', 'inter_type':'str', 'parent_id':'str','child_id':'str', - 'direction':'str', 'condition':'str', 'inc_edge':'str', 'out_edge':'str', - 'end_unix':'int', 'inter_name':'str', 'inter_lat':'float', 'inter_lon':'float', - 'group_no':'int', 'main_phase_no':'int', 'phase_no':'int','ring_type':'str' - } - for alph in ['A', 'B']: - for j in range(1,9): - loading_dtype[f'angle_{alph}{j}'] = 'str' - loading_dtype[f'dura_{alph}{j}'] = 'int' - - self.path_table = os.path.join(self.path_root, 'Data', 'tables') - - self.inter_info = pd.read_csv(os.path.join(self.path_table, 'inter_info.csv'), dtype=loading_dtype) - self.angle = pd.read_csv(os.path.join(self.path_table, 'angle.csv'), dtype=loading_dtype) - self.plan = pd.read_csv(os.path.join(self.path_table, 'plan.csv'), dtype=loading_dtype) - self.inter_node = pd.read_csv(os.path.join(self.path_table, 'inter_node.csv'), dtype=loading_dtype) - self.uturn = pd.read_csv(os.path.join(self.path_table, 'child_uturn.csv'), dtype=loading_dtype) - self.coord = pd.read_csv(os.path.join(self.path_table, 'child_coord.csv'), dtype=loading_dtype) - self.nema = pd.read_csv(os.path.join(self.path_table, 'nema.csv'), encoding='cp949', dtype=loading_dtype) - print("1-2. 테이블들이 로드되었습니다.") - - # 1-3. 테이블 불러오기 - def check_networks(self): - # https://sumo.dlr.de/docs/Netedit/neteditUsageExamples.html#simplify_tls_program_state_after_changing_connections - if 'SUMO_HOME' in os.environ: - tools = os.path.join(os.environ['SUMO_HOME'], 'tools') - if tools not in sys.path: - sys.path.append(tools) - else: - raise EnvironmentError("please declare environment variable 'SUMO_HOME'") - traci.start([sumolib.checkBinary('sumo'), "-n", os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')]) - nodes = [node for node in self.net.getNodes() if node.getType()=='traffic_light'] - for node in nodes: - node_id = node.getID() - from_xml = len([c for c in node.getConnections() if c.getTLLinkIndex() >= 0]) - from_traci = len(traci.trafficlight.getRedYellowGreenState(node_id)) - if from_xml != from_traci: - sub = {'id': node_id, 'type': 'node', 'note': '유효하지 않은 연결이있음. netedit에서 clean states 필요.'} - self.issues.append(sub) - traci.close() - print("1-3. 네트워크의 모든 clean state requirement들을 체크했습니다.") - - # 1-4. 테이블의 무결성 검사 - def check_tables(self): - self.check_inter_info() - self.check_angle() - self.check_plan() - print("1-4. 모든 테이블들의 무결성을 검사했고 이상 없습니다.") - pass - - # 1-4-1. 교차로정보(inter_info) 검사 - def check_inter_info(self): - # 1-4-1-1. inter_lat, inter_lon 적절성 검사 - # self.inter_info.loc[0, 'inter_lat'] = 38.0 # 에러 발생을 위한 코드 - self.max_lon, self.min_lon = 127.207888, 127.012492 - self.max_lat, self.min_lat = 37.480693, 37.337112 - for _, row in self.inter_info.iterrows(): - latbool = self.min_lat <= row['inter_lat'] <= self.max_lat - lonbool = self.min_lon <= row['inter_lon'] <= self.max_lon - if not(latbool and lonbool): - msg = f"1-4-1-1. 위도 또는 경도가 범위를 벗어난 교차로가 있습니다: inter_no : {row['inter_no']}" - self.issues.append(msg) - # 교차로목록 정의 - self.inter_nos = sorted(self.inter_info.inter_no.unique()) - - # 1-4-2. 방위각정보(inter_info) 검사 - def check_angle(self): - # 1-4-2-1. inter_no 검사 - # self.angle.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드 - missing_inter_nos = set(self.angle.inter_no) - set(self.inter_nos) - if missing_inter_nos: - msg = f"1-4-2-1. angle의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}" - self.issues.append(msg) - - # 1-4-3. 신호계획(plan) 검사 - def check_plan(self): - # 1-4-3-1. inter_no 검사 - # self.plan.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드 - missing_inter_nos = set(self.plan.inter_no) - set(self.inter_nos) - if missing_inter_nos: - msg = f"1-4-3-1. plan의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}" - self.issues.append(msg) - - # 1-4-3-2. 시작시각 검사 - # self.plan.loc[0, 'start_hour'] = 27 # 에러 발생을 위한 코드 - for _, row in self.plan.iterrows(): - start_hour = row.start_hour - start_minute = row.start_minute - if not (0 <= start_hour <= 23) or not (0 <= start_minute <= 59): - msg = f"1-4-3-2. plan에 잘못된 형식의 start_time이 존재합니다: {start_hour, start_minute}" - self.issues.append(msg) - - # 1-4-3-3. 현시시간 검사 - # self.plan.loc[0, 'dura_A1'] = -2 # 에러 발생을 위한 코드 - durations = self.plan[[f'dura_{alph}{j}' for alph in ['A','B'] for j in range(1, 9)]] - valid_indices = ((durations >= 0) & (durations <= 200)).all(axis=1) - invalid_inter_nos = sorted(self.plan[~ valid_indices].inter_no.unique()) - if invalid_inter_nos: - msg = f"1-4-3-3. 음수이거나 200보다 큰 현시시간이 존재합니다. : {invalid_inter_nos}" - - # 1-4-3-4. 주기 일관성 검사 - # self.plan.loc[0, 'cycle'] = 50 # 에러 발생을 위한 코드 - inconsistent_cycle = self.plan.groupby(['inter_no', 'start_hour', 'start_minute'])['cycle'].nunique().gt(1) - if inconsistent_cycle.any(): - inc_inter_no, start_hour, start_minute = inconsistent_cycle[inconsistent_cycle].index[0] - msg = f"1-4-3-4. inter_no:{inc_inter_no}, start_hour:{start_minute}, start_hour:{start_minute}일 때, cycle이 유일하게 결정되지 않습니다." - self.issues.append(msg) - - # 1-4-3-5. 현시시간 / 주기 검사 - # self.plan.loc[0, 'duration'] = 10 # 에러 발생을 위한 코드 - right_duration = True - for (inter_no, start_hour, start_minute), group in self.plan.groupby(['inter_no', 'start_hour', 'start_minute']): - A_sum = group[[f'dura_A{j}' for j in range(1, 9)]].iloc[0].sum() - B_sum = group[[f'dura_B{j}' for j in range(1, 9)]].iloc[0].sum() - # A_sum = group[group['ring_type']=='A']['duration'].sum() - # B_sum = group[group['ring_type']=='B']['duration'].sum() - cycle = group['cycle'].unique()[0] - if not (A_sum == B_sum == cycle): - right_duration = False - inc_inter_no = inter_no - if not right_duration: - msg = f"1-4-4-5. inter_no:{inc_inter_no}, A링현시시간의 합과 B링현시시간의 합이 일치하지 않거나, 현시시간의 합과 주기가 일치하지 않습니다." - self.issues.append(msg) - - # 2. 중간산출물 만들기 - def get_intermediates(self): - self.get_matches() - # self.get_movements() - self.get_node2num_cycles() - - # 2-1 매칭테이블들 생성 - def get_matches(self): - self.make_match1() - self.make_match2() - self.make_match3() - self.make_match4() - self.make_match5() - self.make_match6() - self.make_matching() - - # 2-1-1 - def make_match1(self): - ''' - 신호 DB에는 매 초마다 이동류정보가 업데이트 된다. 그리고 이 이동류정보를 매 5초마다 불러와서 사용하게 된다. - '../Data/tables/move/'에는 5초마다의 이동류정보가 저장되어 있다. - - return : 통합된 이동류정보 - - 모든 inter_no(교차로번호)에 대한 A, B링 현시별 이동류정보 - - match1을 만드는 데 시간이 소요되므로 한 번 만들어서 저장해두고 저장해둔 것을 쓴다. - ''' - # [이동류번호] 불러오기 (약 1분의 소요시간) - path_move = os.path.join(self.path_root, 'Data', 'tables', 'move') - csv_moves = os.listdir(path_move) - moves = [pd.read_csv(os.path.join(path_move, csv_move), index_col=0) for csv_move in tqdm(csv_moves, desc='이동류정보 불러오는 중 : match1')] - self.match1 = pd.concat(moves).drop_duplicates().sort_values(by=['inter_no','phas_A','phas_B']).reset_index(drop=True) - self.match1.to_csv(os.path.join(self.path_root, 'Intermediates', 'match1.csv')) - - # 2-1-2 - def make_match2(self): - ''' - match1을 계층화함. - - match1의 컬럼 : inter_no, phas_A, phas_B, move_A, move_B - - match2의 컬럼 : inter_no, phase_no, ring_type, move_no - ''' - # 계층화 (inter_no, phas_A, phas_B, move_A, move_B) -> ('inter_no', 'phase_no', 'ring_type', 'move_no') - matchA = self.match1[['inter_no', 'phas_A', 'move_A']].copy() - matchA.columns = ['inter_no', 'phase_no', 'move_no'] - matchA['ring_type'] = 'A' - matchB = self.match1[['inter_no', 'phas_B', 'move_B']].copy() - matchB.columns = ['inter_no', 'phase_no', 'move_no'] - matchB['ring_type'] = 'B' - self.match2 = pd.concat([matchA, matchB]).drop_duplicates() - self.match2 = self.match2[['inter_no', 'phase_no', 'ring_type', 'move_no']] - self.match2 = self.match2.sort_values(by=list(self.match2.columns)) - - # 2-1-3 - def make_match3(self): - ''' - 각 movement들에 방향(진입방향, 진출방향)을 매칭시켜 추가함. - - match2의 컬럼 : inter_no, phase_no, ring_type, move_no - - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir - - nema : - - 컬럼 : move_no, inc_dir, out_dir - - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 - - 이동류번호 : 1 ~ 16, 17, 18, 21 - - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 - ''' - # nema 정보 불러오기 및 병합 - self.match3 = pd.merge(self.match2, self.nema, how='left', on='move_no').drop_duplicates() - - # 2-1-4 - def make_match4(self): - ''' - 방위각 정보를 매칭시켜 추가함. - - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir - - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle - - angle_original : - - 컬럼 : inter_no, angle_Aj, angle_Bj (j : 1 ~ 8) - - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 - - 이동류번호 : 1 ~ 16, 17, 18, 21 - - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 - ''' - - # 계층화 - angles = [] - for i, row in self.angle.iterrows(): - angle_codes = row[[f'angle_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] - new = pd.DataFrame({'inter_no':[row.inter_no] * 16, 'phase_no':list(range(1, 9))*2, 'ring_type':['A'] * 8 + ['B'] * 8, 'angle_code':angle_codes.to_list()}) - angles.append(new) - angles = pd.concat(angles) - angles = angles.dropna().reset_index(drop=True) - - # 병합 - six_chars = angles.angle_code.apply(lambda x:len(x)==6) - angles.loc[six_chars,'inc_angle'] = angles.angle_code.apply(lambda x:x[:3]) - angles.loc[six_chars,'out_angle'] = angles.angle_code.apply(lambda x:x[3:]) - angles = angles.drop('angle_code', axis=1) - self.match4 = pd.merge(self.match3, angles, how='left', left_on=['inter_no', 'phase_no', 'ring_type'], - right_on=['inter_no', 'phase_no', 'ring_type']).drop_duplicates() - - # 2-1-5 - def make_match5(self): - ''' - 진입엣지id, 진출엣지id, 노드id를 추가함 (주교차로). - - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle - - match5의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id - - 사용된 데이터 : - (1) net - - 성남시 정자동 부근의 샘플 네트워크 - (2) inter_node - - 교차로번호와 노드id를 매칭시키는 테이블. - - parent/child 정보도 포함되어 있음 - - 컬럼 : inter_no, node_id, inter_type - (3) inter_info - - 교차로 정보. 여기에서는 위도와 경도가 쓰임. - - 컬럼 : inter_no, inter_name, inter_lat, inter_lon, group_no, main_phase_no - - 진입엣지id, 진출엣지id를 얻는 과정 : - - match5 = match4.copy()의 각 열을 순회하면서 아래 과정을 반복함. - * 진입에 대해서만 서술하겠지만 진출도 마찬가지로 설명될 수 있음 - - 해당 행의 교차로정보로부터 노드ID를 얻어내고, 해당 노드에 대한 모든 진출엣지id를 inc_edges에 저장. - * inc_edge(진입엣지) : incoming edge, out_edge(진출엣지) : outgoing_edge - - inc_edges의 모든 진입엣지에 대하여 진입방향(inc_dires, 2차원 단위벡터)을 얻어냄. - - 해당 행의 진입각으로부터 그에 대응되는 진입각방향(단위벡터)를 얻어냄. - - 주어진 진입각방향에 대하여 내적이 가장 작은 진입방향에 대한 진입엣지를 inc_edge_id로 지정함. - ''' - - # parent node만 가져옴. - inter_node1 = self.inter_node[self.inter_node.inter_type == 'parent'].drop('inter_type', axis=1) - inter_info1 = self.inter_info[['inter_no', 'inter_lat', 'inter_lon']] - inter = pd.merge(inter_node1, inter_info1, how='left', left_on=['inter_no'], - right_on=['inter_no']).drop_duplicates() - - self.inter2node = dict(zip(inter['inter_no'], inter['node_id'])) - - self.match5 = self.match4.copy() - # 진입진출ID 매칭 - for index, row in self.match5.iterrows(): - node_id = self.inter2node[row.inter_no] - node = self.net.getNode(node_id) - # 교차로의 모든 (from / to) edges - inc_edges = [edge for edge in node.getIncoming() if edge.getFunction() == ''] # incoming edges - out_edges = [edge for edge in node.getOutgoing() if edge.getFunction() == ''] # outgoing edges - # 교차로의 모든 (from / to) directions - inc_dirs = [] - for inc_edge in inc_edges: - start = inc_edge.getShape()[-2] - end = inc_edge.getShape()[-1] - inc_dir = np.array(end) - np.array(start) - inc_dir = inc_dir / (inc_dir ** 2).sum() ** 0.5 - inc_dirs.append(inc_dir) - out_dirs = [] - for out_edge in out_edges: - start = out_edge.getShape()[0] - end = out_edge.getShape()[1] - out_dir = np.array(end) - np.array(start) - out_dir = out_dir / (out_dir ** 2).sum() ** 0.5 - out_dirs.append(out_dir) - # 진입각, 진출각 불러오기 - if not pd.isna(row.inc_angle): - inc_angle = int(row.inc_angle) - out_angle = int(row.out_angle) - # 방위각을 일반각으로 가공, 라디안 변환, 단위벡터로 변환 - inc_angle = (-90 - inc_angle) % 360 - inc_angle = inc_angle * np.pi / 180. - inc_dir_true = np.array([np.cos(inc_angle), np.sin(inc_angle)]) - out_angle = (90 - out_angle) % 360 - out_angle = out_angle * np.pi / 180. - out_dir_true = np.array([np.cos(out_angle), np.sin(out_angle)]) - # 매칭 엣지 반환 - inc_index = np.array([np.dot(inc_dir, inc_dir_true) for inc_dir in inc_dirs]).argmax() - out_index = np.array([np.dot(out_dir, out_dir_true) for out_dir in out_dirs]).argmax() - inc_edge_id = inc_edges[inc_index].getID() - out_edge_id = out_edges[out_index].getID() - self.match5.at[index, 'inc_edge'] = inc_edge_id - self.match5.at[index, 'out_edge'] = out_edge_id - self.match5['node_id'] = self.match5['inter_no'].map(self.inter2node) - self.match5 = self.match5.sort_values(by=['inter_no','phase_no','ring_type']).reset_index(drop=True) - - # 2-1-6 - def make_match6(self): - ''' - 진입엣지id, 진출엣지id, 노드id를 추가함 (부교차로). - - match6의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id - - 사용된 데이터 : - (1) inter_node - - 교차로번호와 노드id를 매칭시키는 테이블. - - parent/child 정보도 포함되어 있음 - - 컬럼 : inter_no, node_id, inter_type - (2) uturn (유턴정보) - - 컬럼 : parent_id, child_id, direction, condition, inc_edge, out_edge - - parent_id, child_id : 주교차로id, 유턴교차로id - - direction : 주교차로에 대한 유턴노드의 상대적인 위치(방향) - - condition : 좌회전시, 직진시, 직좌시, 보행신호시 중 하나 - - inc_edge, out_edge : 유턴에 대한 진입진출엣지 - (3) coord (연동교차로정보) - - 컬럼 : parent_id, child_id, phase_no, ring_type, inc_edge, out_edge - - parent_id, child_id : 주교차로id, 연동교차로id - - 나머지 컬럼 : 각 (현시, 링)별 진입진출엣지 - - 설명 : - - match5는 주교차로에 대해서만 진입엣지id, 진출엣지id, 노드id를 추가했었음. - 여기에서 uturn, coord를 사용해서 부교차로들(유턴교차로, 연동교차로)에 대해서도 해당 값들을 부여함. - 유턴교차로 : - - directions를 정북기준 시계방향의 8방위로 정함. - - 이를 통해 진입방향이 주어진 경우에 좌회전, 직진, 보행 등에 대한 (진입방향, 진출방향)을 얻어낼 수 있음. - - 예) 진입방향(direction)이 '북'일 때, - - 직진 : (북, 남) - * 남 : directions[(ind + 4) % len(directions)] - - 좌회전 : (북, 동) - * 동 : directions[(ind + 2) % len(directions)] - - 보행 : (서, 동) - * 서 : directions[(ind - 2) % len(directions)] - - uturn의 각 행을 순회하면서 아래 과정을 반복함 - - match5에서 parent_id에 해당하는 행들을 가져옴(cmatch). - - condition 별로 진입방향, 진출방향A, 진출방향B 정함. - - 상술한 directions를 활용하여 정함. - - (진입방향, 진출방향A, 진출방향B)을 고려하여 (현시, 링) 별로 진입엣지id, 진출엣지id를 정함. - - ex) cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - - 순회하면서 만든 cmatch를 cmatchs라는 리스트에 저장함. - - 연동교차로 : - - 연동교차로의 경우 coord에 (현시, 링)별 진입엣지ID, 진출엣지ID가 명시되어 있음. - - 'inc_dir', 'out_dir', 'inc_angle','out_angle'와 같은 열들은 np.nan을 지정해놓음. - - 이 열들은, 사실상 다음 스텝부터는 사용되지 않는 열들이기 때문에 np.nan으로 지정해놓아도 문제없음. - - match6 : - - 이렇게 얻은 match5, cmatchs, coord를 모두 pd.concat하여 match6을 얻어냄. - ''' - - self.node2inter = dict(zip(self.inter_node['node_id'], self.inter_node['inter_no'])) - - child_ids = self.inter_node[self.inter_node.inter_type=='child'].node_id.unique() - ch2pa = {} # child to parent - for child_id in child_ids: - parent_no = self.inter_node[self.inter_node.node_id==child_id].inter_no.iloc[0] - sub_inter_node = self.inter_node[self.inter_node.inter_no==parent_no] - ch2pa[child_id] = sub_inter_node[sub_inter_node.inter_type=='parent'].iloc[0].node_id - directions = ['북', '북동', '동', '남동', '남', '남서', '서', '북서'] # 정북기준 시계방향으로 8방향 - - # 각 uturn node에 대하여 (inc_edge_id, out_edge_id) 부여 - cmatches = [] - for _, row in self.uturn.iterrows(): - child_id = row.child_id - parent_id = row.parent_id - direction = row.direction - condition = row.condition - inc_edge_id = row.inc_edge - out_edge_id = row.out_edge - # match5에서 parent_id에 해당하는 행들을 가져옴 - cmatch = self.match5.copy()[self.match5.node_id==parent_id] # match dataframe for a child node - cmatch = cmatch.sort_values(by=['phase_no', 'ring_type']).reset_index(drop=True) - cmatch['node_id'] = child_id - cmatch[['inc_edge', 'out_edge']] = np.nan - - # condition 별로 inc_dire, out_dire_A, out_dire_B를 정함 - ind = directions.index(direction) - if condition == "좌회전시": - inc_dire = direction - out_dire_A = out_dire_B = directions[(ind + 2) % len(directions)] - elif condition == "직진시": - inc_dire = direction - out_dire_A = out_dire_B = directions[(ind + 4) % len(directions)] - elif condition == "보행신호시": - inc_dire = directions[(ind + 2) % len(directions)] - out_dire_A = directions[(ind - 2) % len(directions)] - out_dire_B = directions[(ind - 2) % len(directions)] - - # (inc_dire, out_dire_A, out_dire_B) 별로 inc_edge_id, out_edge_id를 정함 - if condition == '보행신호시': - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - # 이동류번호가 17(보행신호)이면서 유턴노드방향으로 가는 신호가 없으면 (inc_edge_id, out_edge_id)를 부여한다. - cmatch.loc[(cmatch.move_no==17) & (cmatch.out_dir!=direction), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - else: # '직진시', '좌회전시' - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - # 유턴신호의 이동류번호를 19로 부여한다. - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), 'move_no'] = 19 - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), 'move_no'] = 19 - cmatches.append(cmatch) - - # 각 coordination node에 대하여 (inc_edge_id, out_edge_id) 부여 - self.coord['inter_no'] = self.coord['parent_id'].map(self.node2inter) - self.coord = self.coord.rename(columns={'child_id':'node_id'}) - self.coord[['inc_dir', 'out_dir', 'inc_angle','out_angle']] = np.nan - self.coord['move_no'] = 20 - self.coord = self.coord[['inter_no', 'phase_no', 'ring_type', 'move_no', 'inc_dir', 'out_dir', 'inc_angle','out_angle', 'inc_edge', 'out_edge', 'node_id']] - - # display(coord) - cmatches = pd.concat(cmatches) - self.match6 = pd.concat([self.match5, cmatches, self.coord]).drop_duplicates().sort_values(by=['inter_no', 'node_id', 'phase_no', 'ring_type']) - self.match6.to_csv(os.path.join(self.path_root, 'Intermediates', 'match6.csv')) - - # 2-1-7 - def make_matching(self): - ''' - 이동류 매칭 : 각 교차로에 대하여, 가능한 모든 이동류 (1~18, 21)에 대한 진입·진출엣지ID를 지정한다. - 모든 이동류에 대해 지정하므로, 시차제시 이전과 다른 이동류가 등장하더라도 항상 진입·진출 엣지 ID를 지정할 수 있다. - - matching의 컬럼 : inter_no, move_no, inc_dir, out_dir, inc_edge, out_edge, node_id - - 설명 : - - 필요한 리스트, 딕셔너리 등을 정의 - (1) 가능한 (진입방향, 진출방향) 목록 [리스트] - (2) 각 교차로별 방향 목록 : pdires (possible directions) [딕셔너리] - (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) [딕셔너리] - (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) [딕셔너리] - (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) [딕셔너리] - - matching은 빈 리스트로 지정. - - 모든 노드id에 대하여 다음 과정을 반복 - - 해당 노드id에 대한 모든 가능한 (진입방향, 진출방향)에 대하여 다음 과정을 반복 - - (노드id, 진입방향)으로부터 진입엣지id를 얻어냄. 마찬가지로 진출엣지id도 얻어냄 - - 얻어낸 정보를 바탕으로 한 행(new_row)을 만들고 이것을 matching에 append - ''' - - self.match7 = self.match6.copy() - self.match7 = self.match7[['inter_no', 'move_no', 'inc_dir', 'out_dir', 'inc_edge', 'out_edge', 'node_id']] - - parent_ids = sorted(self.inter_node[self.inter_node.inter_type=='parent'].node_id.unique()) - child_ids = sorted(self.inter_node[self.inter_node.inter_type=='child'].node_id.unique()) - - # (1) 가능한 (진입방향, 진출방향) 목록 - flows = self.nema.dropna().apply(lambda row: (row['inc_dir'], row['out_dir']), axis=1).tolist() - # (2) 각 교차로별 방향 목록 : pdires (possible directions) - pdires = {} - for node_id in parent_ids: - dires = self.match7[self.match7.node_id == node_id][['inc_dir','out_dir']].values.flatten() - dires = {dire for dire in dires if type(dire)==str} - pdires[node_id] = dires - # (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) - inc2id = {} - for node_id in parent_ids: - for inc_dir in pdires[node_id]: - df = self.match7[(self.match7.node_id==node_id) & (self.match7.inc_dir==inc_dir)] - inc2id[(node_id, inc_dir)] = df.inc_edge.iloc[0] - # (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) - out2id = {} - for node_id in parent_ids: - for out_dir in pdires[node_id]: - df = self.match7[(self.match7.node_id==node_id) & (self.match7.out_dir==out_dir)] - out2id[(node_id, out_dir)] = df.out_edge.iloc[0] - # (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) - pflow = {} - for node_id in parent_ids: - pflow[node_id] = [flow for flow in flows if set(flow).issubset(pdires[node_id])] - # (6) 가능한 이동류에 대하여 진입id, 진출id 배정 : matching - # node2inter = dict(zip(self.match7['node_id'], self.match7['inter_no'])) - dires_right = ['북', '서', '남', '동', '북'] # ex (북, 서), (서, 남) 등은 우회전 flow - self.matching = [] - for node_id in parent_ids: - inter_no = self.node2inter[node_id] - # 좌회전과 직진(1 ~ 16) - for (inc_dir, out_dir) in pflow[node_id]: - move_no = self.nema[(self.nema.inc_dir==inc_dir) & (self.nema.out_dir==out_dir)].move_no.iloc[0] - inc_edge = inc2id[(node_id, inc_dir)] - out_edge = out2id[(node_id, out_dir)] - new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[move_no], - 'inc_dir':[inc_dir], 'out_dir':[out_dir], - 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) - self.matching.append(new_row) - # 보행신호(17), 전적색(18) - new_row = pd.DataFrame({'inter_no':[inter_no] * 2, 'move_no':[17, 18], - 'inc_dir':[None]*2, 'out_dir':[None]*2, - 'inc_edge':[None]*2, 'out_edge':[None]*2, 'node_id':[node_id]*2}) - self.matching.append(new_row) - # 신호우회전(21) - for d in range(len(dires_right)-1): - inc_dir = dires_right[d] - out_dir = dires_right[d+1] - if {inc_dir, out_dir}.issubset(pdires[node_id]): - inc_edge = inc2id[(node_id, inc_dir)] - out_edge = out2id[(node_id, out_dir)] - new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[21], - 'inc_dir':[inc_dir], 'out_dir':[out_dir], - 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) - self.matching.append(new_row) - self.matching.append(self.match7[self.match7.node_id.isin(child_ids)]) - self.matching = pd.concat(self.matching) - self.matching = self.matching.dropna().sort_values(by=['inter_no', 'node_id', 'move_no']).reset_index(drop=True) - self.matching['move_no'] = self.matching['move_no'].astype(int) - self.matching.to_csv(os.path.join(self.path_root, 'Intermediates', 'matching.csv')) - - # 2-2 - def get_movements(self): - movements_path = os.path.join(self.path_root, 'Intermediates', 'movement') - movements_list = [pd.read_csv(os.path.join(movements_path, file), index_col=0) for file in tqdm(os.listdir(movements_path), desc='이동류정보 불러오는 중 : movements')] - movements = pd.concat(movements_list) - movements = movements.drop(columns=['start_unix']) - movements = movements.drop_duplicates() - movements = movements.sort_values(by=['inter_no', 'phas_A', 'phas_B']) - movements = movements.reset_index(drop=True) - movements.to_csv(os.path.join(self.path_root, 'Intermediates', 'movements.csv')) - return movements - - # 2-3 node2num_cycles : A dictionary that maps a node_id to the number of cycles - def get_node2num_cycles(self): - # node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no'])) - self.node_ids = sorted(self.inter_node.node_id.unique()) - - Aplan = self.plan.copy()[['inter_no'] + [f'dura_A{j}' for j in range(1,9)] + ['cycle']] - grouped = Aplan.groupby('inter_no') - df = grouped.agg({'cycle': 'min'}).reset_index() - df = df.rename(columns={'cycle': 'min_cycle'}) - df['num_cycle'] = 300 // df['min_cycle'] + 2 - inter2num_cycles = dict(zip(df['inter_no'], df['num_cycle'])) - node2numcycles = {node_id : inter2num_cycles[self.node2inter[node_id]] for node_id in self.node_ids} - with open(os.path.join('Intermediates','node2numcycles.json'), 'w') as file: - json.dump(node2numcycles, file, indent=4) - return node2numcycles - - # 3. 이슈사항 저장 - def write_issues(self): - path_issues = os.path.join(self.path_root, "Results", "issues_intermediates.txt") - with open(path_issues, "w", encoding="utf-8") as file: - for item in self.issues: - file.write(item + "\n") - if self.issues: - print("데이터 처리 중 발생한 특이사항은 다음과 같습니다. :") - for review in self.issues: - print(review) - - def main(self): - # 1. 데이터 불러오기 - self.load_data() - # 2. 중간산출물 만들기 - self.get_intermediates() - # 3. 이슈사항 저장 - self.write_issues() - -if __name__ == '__main__': - self = DailyPreprocess() - self.main() \ No newline at end of file diff --git a/Script/preprocess_5min.ipynb b/Script/preprocess_5min.ipynb index 1a7c91a6a..a03091149 100644 --- a/Script/preprocess_5min.ipynb +++ b/Script/preprocess_5min.ipynb @@ -41,7 +41,6 @@ "metadata": {}, "outputs": [], "source": [ - "# 5초 단위로 이동류번호 저장 및 신호이력에서 유닉스시각 가져와서 표시, 한시간동안의 데이터만 보관\n", "midnight = int(datetime(2024, 1, 5, 0, 0, 0).timestamp())\n", "next_day = int(datetime(2024, 1, 6, 0, 0, 0).timestamp())\n", "fsecs = range(midnight, next_day, 5) # fsecs : unix time by Five SECondS\n", @@ -581,24 +580,6 @@ " return movement_updated" ] }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def make_movements():\n", - " movements_path = '../Intermediates/movement/'\n", - " movements_list = [pd.read_csv(movements_path + file, index_col=0) for file in tqdm(os.listdir(movements_path))]\n", - " movements = pd.concat(movements_list)\n", - " movements = movements.drop(columns=['start_unix'])\n", - " movements = movements.drop_duplicates()\n", - " movements = movements.sort_values(by=['inter_no', 'phas_A', 'phas_B'])\n", - " movements = movements.reset_index(drop=True)\n", - " movements.to_csv('../Intermediates/movements.csv')\n", - " return movements" - ] - }, { "cell_type": "code", "execution_count": 13, diff --git a/Script/preprocess_daily.py b/Script/preprocess_daily.py index f0de51881..0b4993465 100644 --- a/Script/preprocess_daily.py +++ b/Script/preprocess_daily.py @@ -1,431 +1,592 @@ import pandas as pd import numpy as np -import os +import os, sys import json -import sumolib +import sumolib, traci from tqdm import tqdm -def check_inter_info(inter_info): - print(inter_info) - print('check') - -def make_match1(path_root): - ''' - 신호 DB에는 매 초마다 이동류정보가 업데이트 된다. 그리고 이 이동류정보를 매 5초마다 불러와서 사용하게 된다. - '../Data/tables/move/'에는 5초마다의 이동류정보가 저장되어 있다. - - return : 통합된 이동류정보 - - 모든 inter_no(교차로번호)에 대한 A, B링 현시별 이동류정보 - - match1을 만드는 데 시간이 소요되므로 한 번 만들어서 저장해두고 저장해둔 것을 쓴다. - ''' - # [이동류번호] 불러오기 (약 1분의 소요시간) - path_move = os.path.join(path_root, 'Data', 'tables', 'move') - csv_moves = os.listdir(path_move) - moves = [pd.read_csv(os.path.join(path_move, csv_move), index_col=0) for csv_move in tqdm(csv_moves, desc='이동류정보 불러오는 중 : match1')] - match1 = pd.concat(moves).drop_duplicates().sort_values(by=['inter_no','phas_A','phas_B']).reset_index(drop=True) - match1.to_csv(os.path.join(path_root, 'Intermediates', 'match1.csv')) - return match1 - -def make_match2(match1): - ''' - match1을 계층화함. - - match1의 컬럼 : inter_no, phas_A, phas_B, move_A, move_B - - match2의 컬럼 : inter_no, phase_no, ring_type, move_no - ''' - # 계층화 (inter_no, phas_A, phas_B, move_A, move_B) -> ('inter_no', 'phase_no', 'ring_type', 'move_no') - matchA = match1[['inter_no', 'phas_A', 'move_A']].copy() - matchA.columns = ['inter_no', 'phase_no', 'move_no'] - matchA['ring_type'] = 'A' - matchB = match1[['inter_no', 'phas_B', 'move_B']].copy() - matchB.columns = ['inter_no', 'phase_no', 'move_no'] - matchB['ring_type'] = 'B' - match2 = pd.concat([matchA, matchB]).drop_duplicates() - match2 = match2[['inter_no', 'phase_no', 'ring_type', 'move_no']] - match2 = match2.sort_values(by=list(match2.columns)) - return match2 - -def make_match3(match2, nema): - ''' - 각 movement들에 방향(진입방향, 진출방향)을 매칭시켜 추가함. - - match2의 컬럼 : inter_no, phase_no, ring_type, move_no - - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir - - nema : - - 컬럼 : move_no, inc_dir, out_dir - - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 - - 이동류번호 : 1 ~ 16, 17, 18, 21 - - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 - ''' - # nema 정보 불러오기 및 병합 - match3 = pd.merge(match2, nema, how='left', on='move_no').drop_duplicates() - return match3 - -def make_match4(match3, angle): - ''' - 방위각 정보를 매칭시켜 추가함. - - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir - - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle - - angle_original : - - 컬럼 : inter_no, angle_Aj, angle_Bj (j : 1 ~ 8) - - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 - - 이동류번호 : 1 ~ 16, 17, 18, 21 - - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 - ''' - - # 계층화 - angles = [] - for i, row in angle.iterrows(): - angle_codes = row[[f'angle_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] - new = pd.DataFrame({'inter_no':[row.inter_no] * 16, 'phase_no':list(range(1, 9))*2, 'ring_type':['A'] * 8 + ['B'] * 8, 'angle_code':angle_codes.to_list()}) - angles.append(new) - angles = pd.concat(angles) - angles = angles.dropna().reset_index(drop=True) - - # 병합 - six_chars = angles.angle_code.apply(lambda x:len(x)==6) - angles.loc[six_chars,'inc_angle'] = angles.angle_code.apply(lambda x:x[:3]) - angles.loc[six_chars,'out_angle'] = angles.angle_code.apply(lambda x:x[3:]) - angles = angles.drop('angle_code', axis=1) - match4 = pd.merge(match3, angles, how='left', left_on=['inter_no', 'phase_no', 'ring_type'], - right_on=['inter_no', 'phase_no', 'ring_type']).drop_duplicates() - return match4 - -def make_match5(match4, net, inter_node, inter_info): - ''' - 진입엣지id, 진출엣지id, 노드id를 추가함 (주교차로). - - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle - - match5의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id +class DailyPreprocessor(): + def __init__(self): + self.path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.issues = [] + + # 1. 데이터 불러오기 + def load_data(self): + print('1. 데이터를 로드합니다.') + self.load_networks() + self.load_tables() + self.check_networks() + self.check_tables() + + # 1-1. 네트워크 불러오기 + def load_networks(self): + self.net = sumolib.net.readNet(os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')) + print("1-1. 네트워크가 로드되었습니다.") + + # 1-2. 테이블 불러오기 + def load_tables(self): + # 모든 컬럼에 대하여 데이터타입 지정 + loading_dtype = { + 'inter_no':'int', 'start_hour':'int', 'start_minute':'int', 'cycle':'int','offset':'int', + 'node_id':'str', 'inter_type':'str', 'parent_id':'str','child_id':'str', + 'direction':'str', 'condition':'str', 'inc_edge':'str', 'out_edge':'str', + 'end_unix':'int', 'inter_name':'str', 'inter_lat':'float', 'inter_lon':'float', + 'group_no':'int', 'main_phase_no':'int', 'phase_no':'int','ring_type':'str' + } + for alph in ['A', 'B']: + for j in range(1,9): + loading_dtype[f'angle_{alph}{j}'] = 'str' + loading_dtype[f'dura_{alph}{j}'] = 'int' + + self.path_table = os.path.join(self.path_root, 'Data', 'tables') + + # 테이블 불러오기 + self.inter_info = pd.read_csv(os.path.join(self.path_table, 'inter_info.csv'), dtype=loading_dtype) + self.angle = pd.read_csv(os.path.join(self.path_table, 'angle.csv'), dtype=loading_dtype) + self.plan = pd.read_csv(os.path.join(self.path_table, 'plan.csv'), dtype=loading_dtype) + self.inter_node = pd.read_csv(os.path.join(self.path_table, 'inter_node.csv'), dtype=loading_dtype) + self.uturn = pd.read_csv(os.path.join(self.path_table, 'child_uturn.csv'), dtype=loading_dtype) + self.coord = pd.read_csv(os.path.join(self.path_table, 'child_coord.csv'), dtype=loading_dtype) + self.nema = pd.read_csv(os.path.join(self.path_table, 'nema.csv'), encoding='cp949', dtype=loading_dtype) + + # 교차로목록 정의 + self.inter_nos = sorted(self.inter_info.inter_no.unique()) + print("1-2. 테이블들이 로드되었습니다.") + + # 1-3. 네트워크 무결성 검사 + def check_networks(self): + # https://sumo.dlr.de/docs/Netedit/neteditUsageExamples.html#simplify_tls_program_state_after_changing_connections + if 'SUMO_HOME' in os.environ: + tools = os.path.join(os.environ['SUMO_HOME'], 'tools') + if tools not in sys.path: + sys.path.append(tools) + else: + raise EnvironmentError("please declare environment variable 'SUMO_HOME'") + traci.start([sumolib.checkBinary('sumo'), "-n", os.path.join(self.path_root, 'Data', 'networks', 'sn.net.xml')]) + nodes = [node for node in self.net.getNodes() if node.getType()=='traffic_light'] + for node in nodes: + node_id = node.getID() + from_xml = len([c for c in node.getConnections() if c.getTLLinkIndex() >= 0]) + from_traci = len(traci.trafficlight.getRedYellowGreenState(node_id)) + if from_xml != from_traci: + sub = {'id': node_id, 'type': 'node', 'note': '유효하지 않은 연결이있음. netedit에서 clean states 필요.'} + self.issues.append(sub) + traci.close() + print("1-3. 네트워크의 모든 clean state requirement들을 체크했습니다.") - 사용된 데이터 : - (1) net - - 성남시 정자동 부근의 샘플 네트워크 - (2) inter_node - - 교차로번호와 노드id를 매칭시키는 테이블. - - parent/child 정보도 포함되어 있음 - - 컬럼 : inter_no, node_id, inter_type - (3) inter_info - - 교차로 정보. 여기에서는 위도와 경도가 쓰임. - - 컬럼 : inter_no, inter_name, inter_lat, inter_lon, group_no, main_phase_no - - 진입엣지id, 진출엣지id를 얻는 과정 : - - match5 = match4.copy()의 각 열을 순회하면서 아래 과정을 반복함. - * 진입에 대해서만 서술하겠지만 진출도 마찬가지로 설명될 수 있음 - - 해당 행의 교차로정보로부터 노드ID를 얻어내고, 해당 노드에 대한 모든 진출엣지id를 inc_edges에 저장. - * inc_edge(진입엣지) : incoming edge, out_edge(진출엣지) : outgoing_edge - - inc_edges의 모든 진입엣지에 대하여 진입방향(inc_dires, 2차원 단위벡터)을 얻어냄. - - 해당 행의 진입각으로부터 그에 대응되는 진입각방향(단위벡터)를 얻어냄. - - 주어진 진입각방향에 대하여 내적이 가장 작은 진입방향에 대한 진입엣지를 inc_edge_id로 지정함. - ''' - - # parent node만 가져옴. - inter_node1 = inter_node[inter_node.inter_type == 'parent'].drop('inter_type', axis=1) - inter_info1 = inter_info[['inter_no', 'inter_lat', 'inter_lon']] - inter = pd.merge(inter_node1, inter_info1, how='left', left_on=['inter_no'], - right_on=['inter_no']).drop_duplicates() - - inter2node = dict(zip(inter['inter_no'], inter['node_id'])) - - match5 = match4.copy() - # 진입진출ID 매칭 - for index, row in match5.iterrows(): - node_id = inter2node[row.inter_no] - node = net.getNode(node_id) - # 교차로의 모든 (from / to) edges - inc_edges = [edge for edge in node.getIncoming() if edge.getFunction() == ''] # incoming edges - out_edges = [edge for edge in node.getOutgoing() if edge.getFunction() == ''] # outgoing edges - # 교차로의 모든 (from / to) directions - inc_dirs = [] - for inc_edge in inc_edges: - start = inc_edge.getShape()[-2] - end = inc_edge.getShape()[-1] - inc_dir = np.array(end) - np.array(start) - inc_dir = inc_dir / (inc_dir ** 2).sum() ** 0.5 - inc_dirs.append(inc_dir) - out_dirs = [] - for out_edge in out_edges: - start = out_edge.getShape()[0] - end = out_edge.getShape()[1] - out_dir = np.array(end) - np.array(start) - out_dir = out_dir / (out_dir ** 2).sum() ** 0.5 - out_dirs.append(out_dir) - # 진입각, 진출각 불러오기 - if not pd.isna(row.inc_angle): - inc_angle = int(row.inc_angle) - out_angle = int(row.out_angle) - # 방위각을 일반각으로 가공, 라디안 변환, 단위벡터로 변환 - inc_angle = (-90 - inc_angle) % 360 - inc_angle = inc_angle * np.pi / 180. - inc_dir_true = np.array([np.cos(inc_angle), np.sin(inc_angle)]) - out_angle = (90 - out_angle) % 360 - out_angle = out_angle * np.pi / 180. - out_dir_true = np.array([np.cos(out_angle), np.sin(out_angle)]) - # 매칭 엣지 반환 - inc_index = np.array([np.dot(inc_dir, inc_dir_true) for inc_dir in inc_dirs]).argmax() - out_index = np.array([np.dot(out_dir, out_dir_true) for out_dir in out_dirs]).argmax() - inc_edge_id = inc_edges[inc_index].getID() - out_edge_id = out_edges[out_index].getID() - match5.at[index, 'inc_edge'] = inc_edge_id - match5.at[index, 'out_edge'] = out_edge_id - match5['node_id'] = match5['inter_no'].map(inter2node) - match5 = match5.sort_values(by=['inter_no','phase_no','ring_type']).reset_index(drop=True) - return match5 - -def make_match6(match5, inter_node, uturn, coord, path_root): - ''' - 진입엣지id, 진출엣지id, 노드id를 추가함 (부교차로). - - match6의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id - - 사용된 데이터 : - (1) inter_node - - 교차로번호와 노드id를 매칭시키는 테이블. - - parent/child 정보도 포함되어 있음 - - 컬럼 : inter_no, node_id, inter_type - (2) uturn (유턴정보) - - 컬럼 : parent_id, child_id, direction, condition, inc_edge, out_edge - - parent_id, child_id : 주교차로id, 유턴교차로id - - direction : 주교차로에 대한 유턴노드의 상대적인 위치(방향) - - condition : 좌회전시, 직진시, 직좌시, 보행신호시 중 하나 - - inc_edge, out_edge : 유턴에 대한 진입진출엣지 - (3) coord (연동교차로정보) - - 컬럼 : parent_id, child_id, phase_no, ring_type, inc_edge, out_edge - - parent_id, child_id : 주교차로id, 연동교차로id - - 나머지 컬럼 : 각 (현시, 링)별 진입진출엣지 - - 설명 : - - match5는 주교차로에 대해서만 진입엣지id, 진출엣지id, 노드id를 추가했었음. - 여기에서 uturn, coord를 사용해서 부교차로들(유턴교차로, 연동교차로)에 대해서도 해당 값들을 부여함. - 유턴교차로 : - - directions를 정북기준 시계방향의 8방위로 정함. - - 이를 통해 진입방향이 주어진 경우에 좌회전, 직진, 보행 등에 대한 (진입방향, 진출방향)을 얻어낼 수 있음. - - 예) 진입방향(direction)이 '북'일 때, - - 직진 : (북, 남) - * 남 : directions[(ind + 4) % len(directions)] - - 좌회전 : (북, 동) - * 동 : directions[(ind + 2) % len(directions)] - - 보행 : (서, 동) - * 서 : directions[(ind - 2) % len(directions)] - - uturn의 각 행을 순회하면서 아래 과정을 반복함 - - match5에서 parent_id에 해당하는 행들을 가져옴(cmatch). - - condition 별로 진입방향, 진출방향A, 진출방향B 정함. - - 상술한 directions를 활용하여 정함. - - (진입방향, 진출방향A, 진출방향B)을 고려하여 (현시, 링) 별로 진입엣지id, 진출엣지id를 정함. - - ex) cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - - 순회하면서 만든 cmatch를 cmatchs라는 리스트에 저장함. - - 연동교차로 : - - 연동교차로의 경우 coord에 (현시, 링)별 진입엣지ID, 진출엣지ID가 명시되어 있음. - - 'inc_dir', 'out_dir', 'inc_angle','out_angle'와 같은 열들은 np.nan을 지정해놓음. - - 이 열들은, 사실상 다음 스텝부터는 사용되지 않는 열들이기 때문에 np.nan으로 지정해놓아도 문제없음. - - match6 : - - 이렇게 얻은 match5, cmatchs, coord를 모두 pd.concat하여 match6을 얻어냄. - ''' - - node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no'])) - - child_ids = inter_node[inter_node.inter_type=='child'].node_id.unique() - ch2pa = {} # child to parent - for child_id in child_ids: - parent_no = inter_node[inter_node.node_id==child_id].inter_no.iloc[0] - sub_inter_node = inter_node[inter_node.inter_no==parent_no] - ch2pa[child_id] = sub_inter_node[sub_inter_node.inter_type=='parent'].iloc[0].node_id - directions = ['북', '북동', '동', '남동', '남', '남서', '서', '북서'] # 정북기준 시계방향으로 8방향 - - # 각 uturn node에 대하여 (inc_edge_id, out_edge_id) 부여 - cmatches = [] - for _, row in uturn.iterrows(): - child_id = row.child_id - parent_id = row.parent_id - direction = row.direction - condition = row.condition - inc_edge_id = row.inc_edge - out_edge_id = row.out_edge - # match5에서 parent_id에 해당하는 행들을 가져옴 - cmatch = match5.copy()[match5.node_id==parent_id] # match dataframe for a child node - cmatch = cmatch.sort_values(by=['phase_no', 'ring_type']).reset_index(drop=True) - cmatch['node_id'] = child_id - cmatch[['inc_edge', 'out_edge']] = np.nan - - # condition 별로 inc_dire, out_dire_A, out_dire_B를 정함 - ind = directions.index(direction) - if condition == "좌회전시": - inc_dire = direction - out_dire_A = out_dire_B = directions[(ind + 2) % len(directions)] - elif condition == "직진시": - inc_dire = direction - out_dire_A = out_dire_B = directions[(ind + 4) % len(directions)] - elif condition == "보행신호시": - inc_dire = directions[(ind + 2) % len(directions)] - out_dire_A = directions[(ind - 2) % len(directions)] - out_dire_B = directions[(ind - 2) % len(directions)] - - # (inc_dire, out_dire_A, out_dire_B) 별로 inc_edge_id, out_edge_id를 정함 - if condition == '보행신호시': - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - # 이동류번호가 17(보행신호)이면서 유턴노드방향으로 가는 신호가 없으면 (inc_edge_id, out_edge_id)를 부여한다. - cmatch.loc[(cmatch.move_no==17) & (cmatch.out_dir!=direction), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - else: # '직진시', '좌회전시' - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] - # 유턴신호의 이동류번호를 19로 부여한다. - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), 'move_no'] = 19 - cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), 'move_no'] = 19 - cmatches.append(cmatch) - - # 각 coordination node에 대하여 (inc_edge_id, out_edge_id) 부여 - coord['inter_no'] = coord['parent_id'].map(node2inter) - coord = coord.rename(columns={'child_id':'node_id'}) - coord[['inc_dir', 'out_dir', 'inc_angle','out_angle']] = np.nan - coord['move_no'] = 20 - coord = coord[['inter_no', 'phase_no', 'ring_type', 'move_no', 'inc_dir', 'out_dir', 'inc_angle','out_angle', 'inc_edge', 'out_edge', 'node_id']] - - # display(coord) - cmatches = pd.concat(cmatches) - match6 = pd.concat([match5, cmatches, coord]).drop_duplicates().sort_values(by=['inter_no', 'node_id', 'phase_no', 'ring_type']) - match6.to_csv(os.path.join(path_root, 'Intermediates', 'match6.csv')) - return match6 - -def make_matching(match6, inter_node, nema, path_root): - ''' - 이동류 매칭 : 각 교차로에 대하여, 가능한 모든 이동류 (1~18, 21)에 대한 진입·진출엣지ID를 지정한다. - 모든 이동류에 대해 지정하므로, 시차제시 이전과 다른 이동류가 등장하더라도 항상 진입·진출 엣지 ID를 지정할 수 있다. - - matching의 컬럼 : inter_no, move_no, inc_dir, out_dir, inc_edge, out_edge, node_id - - 설명 : - - 필요한 리스트, 딕셔너리 등을 정의 - (1) 가능한 (진입방향, 진출방향) 목록 [리스트] - (2) 각 교차로별 방향 목록 : pdires (possible directions) [딕셔너리] - (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) [딕셔너리] - (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) [딕셔너리] - (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) [딕셔너리] - - matching은 빈 리스트로 지정. - - 모든 노드id에 대하여 다음 과정을 반복 - - 해당 노드id에 대한 모든 가능한 (진입방향, 진출방향)에 대하여 다음 과정을 반복 - - (노드id, 진입방향)으로부터 진입엣지id를 얻어냄. 마찬가지로 진출엣지id도 얻어냄 - - 얻어낸 정보를 바탕으로 한 행(new_row)을 만들고 이것을 matching에 append - ''' - - match7 = match6.copy() - match7 = match7[['inter_no', 'move_no', 'inc_dir', 'out_dir', 'inc_edge', 'out_edge', 'node_id']] - - parent_ids = sorted(inter_node[inter_node.inter_type=='parent'].node_id.unique()) - child_ids = sorted(inter_node[inter_node.inter_type=='child'].node_id.unique()) - - # (1) 가능한 (진입방향, 진출방향) 목록 - flows = nema.dropna().apply(lambda row: (row['inc_dir'], row['out_dir']), axis=1).tolist() - # (2) 각 교차로별 방향 목록 : pdires (possible directions) - pdires = {} - for node_id in parent_ids: - dires = match7[match7.node_id == node_id][['inc_dir','out_dir']].values.flatten() - dires = {dire for dire in dires if type(dire)==str} - pdires[node_id] = dires - # (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) - inc2id = {} - for node_id in parent_ids: - for inc_dir in pdires[node_id]: - df = match7[(match7.node_id==node_id) & (match7.inc_dir==inc_dir)] - inc2id[(node_id, inc_dir)] = df.inc_edge.iloc[0] - # (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) - out2id = {} - for node_id in parent_ids: - for out_dir in pdires[node_id]: - df = match7[(match7.node_id==node_id) & (match7.out_dir==out_dir)] - out2id[(node_id, out_dir)] = df.out_edge.iloc[0] - # (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) - pflow = {} - for node_id in parent_ids: - pflow[node_id] = [flow for flow in flows if set(flow).issubset(pdires[node_id])] - # (6) 가능한 이동류에 대하여 진입id, 진출id 배정 : matching - node2inter = dict(zip(match7['node_id'], match7['inter_no'])) - dires_right = ['북', '서', '남', '동', '북'] # ex (북, 서), (서, 남) 등은 우회전 flow - matching = [] - for node_id in parent_ids: - inter_no = node2inter[node_id] - # 좌회전과 직진(1 ~ 16) - for (inc_dir, out_dir) in pflow[node_id]: - move_no = nema[(nema.inc_dir==inc_dir) & (nema.out_dir==out_dir)].move_no.iloc[0] - inc_edge = inc2id[(node_id, inc_dir)] - out_edge = out2id[(node_id, out_dir)] - new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[move_no], - 'inc_dir':[inc_dir], 'out_dir':[out_dir], - 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) - matching.append(new_row) - # 보행신호(17), 전적색(18) - new_row = pd.DataFrame({'inter_no':[inter_no] * 2, 'move_no':[17, 18], - 'inc_dir':[None]*2, 'out_dir':[None]*2, - 'inc_edge':[None]*2, 'out_edge':[None]*2, 'node_id':[node_id]*2}) - matching.append(new_row) - # 신호우회전(21) - for d in range(len(dires_right)-1): - inc_dir = dires_right[d] - out_dir = dires_right[d+1] - if {inc_dir, out_dir}.issubset(pdires[node_id]): + # 1-4. 테이블 무결성 검사 + def check_tables(self): + self.check_plan() + self.check_inter_info() + self.check_angle() + print("1-4. 테이블들의 무결성 검사를 완료했습니다.") + pass + + # 1-4-1. 신호계획(plan) 검사 + def check_plan(self): + # 1-4-1-1. inter_no 검사 + # self.plan.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드 + missing_inter_nos = set(self.plan.inter_no) - set(self.inter_nos) + if missing_inter_nos: + msg = f"1-4-1-1. plan의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}" + self.issues.append(msg) + + # 1-4-1-2. 시작시각 검사 + # self.plan.loc[0, 'start_hour'] = 27 # 에러 발생을 위한 코드 + for _, row in self.plan.iterrows(): + start_hour = row.start_hour + start_minute = row.start_minute + if not (0 <= start_hour <= 23) or not (0 <= start_minute <= 59): + msg = f"1-4-1-2. plan에 잘못된 형식의 start_time이 존재합니다: {start_hour, start_minute}" + self.issues.append(msg) + + # 1-4-1-3. 현시시간 검사 + # self.plan.loc[0, 'dura_A1'] = -2 # 에러 발생을 위한 코드 + durations = self.plan[[f'dura_{alph}{j}' for alph in ['A','B'] for j in range(1, 9)]] + valid_indices = ((durations >= 0) & (durations <= 200)).all(axis=1) + invalid_inter_nos = sorted(self.plan[~ valid_indices].inter_no.unique()) + if invalid_inter_nos: + msg = f"1-4-1-3. 음수이거나 200보다 큰 현시시간이 존재합니다. : {invalid_inter_nos}" + + # 1-4-1-4. 주기 일관성 검사 + # self.plan.loc[0, 'cycle'] = 50 # 에러 발생을 위한 코드 + inconsistent_cycle = self.plan.groupby(['inter_no', 'start_hour', 'start_minute'])['cycle'].nunique().gt(1) + if inconsistent_cycle.any(): + inc_inter_no, start_hour, start_minute = inconsistent_cycle[inconsistent_cycle].index[0] + msg = f"1-4-1-4. 한 프로그램에 서로 다른 주기가 존재합니다. inter_no:{inc_inter_no}, start_hour:{start_minute}, start_hour:{start_minute}일 때, cycle이 유일하게 결정되지 않습니다." + self.issues.append(msg) + + # 1-4-1-5. 현시시간 / 주기 검사 + # self.plan.loc[0, 'duration'] = 10 # 에러 발생을 위한 코드 + right_duration = True + for (inter_no, start_hour, start_minute), group in self.plan.groupby(['inter_no', 'start_hour', 'start_minute']): + A_sum = group[[f'dura_A{j}' for j in range(1, 9)]].iloc[0].sum() + B_sum = group[[f'dura_B{j}' for j in range(1, 9)]].iloc[0].sum() + # A_sum = group[group['ring_type']=='A']['duration'].sum() + # B_sum = group[group['ring_type']=='B']['duration'].sum() + cycle = group['cycle'].unique()[0] + if not (A_sum == B_sum == cycle): + right_duration = False + inc_inter_no = inter_no + if not right_duration: + msg = f"1-4-1-5. inter_no:{inc_inter_no}, A링현시시간의 합과 B링현시시간의 합이 일치하지 않거나, 현시시간의 합과 주기가 일치하지 않습니다." + self.issues.append(msg) + + # 1-4-2. 교차로정보(inter_info) 검사 + def check_inter_info(self): + # 1-4-2-1. inter_lat, inter_lon 적절성 검사 + # self.inter_info.loc[0, 'inter_lat'] = 38.0 # 에러 발생을 위한 코드 + self.max_lon, self.min_lon = 127.207888, 127.012492 + self.max_lat, self.min_lat = 37.480693, 37.337112 + for _, row in self.inter_info.iterrows(): + latbool = self.min_lat <= row['inter_lat'] <= self.max_lat + lonbool = self.min_lon <= row['inter_lon'] <= self.max_lon + if not(latbool and lonbool): + msg = f"1-4-2-1. 위도 또는 경도가 범위를 벗어난 교차로가 있습니다: inter_no : {row['inter_no']}" + self.issues.append(msg) + + # 1-4-3. 방위각정보(inter_info) 검사 + def check_angle(self): + # 1-4-3-1. inter_no 검사 + # self.angle.loc[0, 'inter_no'] = '4' # 에러 발생을 위한 코드 + missing_inter_nos = set(self.angle.inter_no) - set(self.inter_nos) + if missing_inter_nos: + msg = f"1-4-2-1. angle의 inter_no 중 교차로 목록(inter_nos)에 포함되지 않는 항목이 있습니다: {missing_inter_nos}" + self.issues.append(msg) + + # 2. 중간산출물 만들기 + def get_intermediates(self): + print('2. 중간산출물을 생성합니다.') + self.get_matches() + self.get_movements() + self.get_node2num_cycles() + + # 2-1 매칭테이블들 생성 + def get_matches(self): + self.make_match1() + self.make_match2() + self.make_match3() + self.make_match4() + self.make_match5() + self.make_match6() + self.make_matching() + print('2-1. 매칭 테이블들을 생성했습니다.') + + # 2-1-1 + def make_match1(self): + ''' + 신호 DB에는 매 초마다 이동류정보가 업데이트 된다. 그리고 이 이동류정보를 매 5초마다 불러와서 사용하게 된다. + '../Data/tables/move/'에는 5초마다의 이동류정보가 저장되어 있다. + + return : 통합된 이동류정보 + - 모든 inter_no(교차로번호)에 대한 A, B링 현시별 이동류정보 + + match1을 만드는 데 시간이 소요되므로 한 번 만들어서 저장해두고 저장해둔 것을 쓴다. + ''' + # [이동류번호] 불러오기 (약 1분의 소요시간) + path_move = os.path.join(self.path_root, 'Data', 'tables', 'move') + csv_moves = os.listdir(path_move) + moves = [pd.read_csv(os.path.join(path_move, csv_move), index_col=0) for csv_move in tqdm(csv_moves, desc='이동류정보 불러오는 중 : match1')] + self.match1 = pd.concat(moves).drop_duplicates().sort_values(by=['inter_no','phas_A','phas_B']).reset_index(drop=True) + self.match1.to_csv(os.path.join(self.path_root, 'Intermediates', 'match1.csv')) + + # 2-1-2 + def make_match2(self): + ''' + match1을 계층화함. + - match1의 컬럼 : inter_no, phas_A, phas_B, move_A, move_B + - match2의 컬럼 : inter_no, phase_no, ring_type, move_no + ''' + # 계층화 (inter_no, phas_A, phas_B, move_A, move_B) -> ('inter_no', 'phase_no', 'ring_type', 'move_no') + matchA = self.match1[['inter_no', 'phas_A', 'move_A']].copy() + matchA.columns = ['inter_no', 'phase_no', 'move_no'] + matchA['ring_type'] = 'A' + matchB = self.match1[['inter_no', 'phas_B', 'move_B']].copy() + matchB.columns = ['inter_no', 'phase_no', 'move_no'] + matchB['ring_type'] = 'B' + self.match2 = pd.concat([matchA, matchB]).drop_duplicates() + self.match2 = self.match2[['inter_no', 'phase_no', 'ring_type', 'move_no']] + self.match2 = self.match2.sort_values(by=list(self.match2.columns)) + + # 2-1-3 + def make_match3(self): + ''' + 각 movement들에 방향(진입방향, 진출방향)을 매칭시켜 추가함. + - match2의 컬럼 : inter_no, phase_no, ring_type, move_no + - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir + + nema : + - 컬럼 : move_no, inc_dir, out_dir + - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 + - 이동류번호 : 1 ~ 16, 17, 18, 21 + - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 + ''' + # nema 정보 불러오기 및 병합 + self.match3 = pd.merge(self.match2, self.nema, how='left', on='move_no').drop_duplicates() + + # 2-1-4 + def make_match4(self): + ''' + 방위각 정보를 매칭시켜 추가함. + - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir + - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle + + angle_original : + - 컬럼 : inter_no, angle_Aj, angle_Bj (j : 1 ~ 8) + - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 + - 이동류번호 : 1 ~ 16, 17, 18, 21 + - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 + ''' + + # 계층화 + angles = [] + for i, row in self.angle.iterrows(): + angle_codes = row[[f'angle_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] + new = pd.DataFrame({'inter_no':[row.inter_no] * 16, 'phase_no':list(range(1, 9))*2, 'ring_type':['A'] * 8 + ['B'] * 8, 'angle_code':angle_codes.to_list()}) + angles.append(new) + angles = pd.concat(angles) + angles = angles.dropna().reset_index(drop=True) + + # 병합 + six_chars = angles.angle_code.apply(lambda x:len(x)==6) + angles.loc[six_chars,'inc_angle'] = angles.angle_code.apply(lambda x:x[:3]) + angles.loc[six_chars,'out_angle'] = angles.angle_code.apply(lambda x:x[3:]) + angles = angles.drop('angle_code', axis=1) + self.match4 = pd.merge(self.match3, angles, how='left', left_on=['inter_no', 'phase_no', 'ring_type'], + right_on=['inter_no', 'phase_no', 'ring_type']).drop_duplicates() + + # 2-1-5 + def make_match5(self): + ''' + 진입엣지id, 진출엣지id, 노드id를 추가함 (주교차로). + - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle + - match5의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id + + 사용된 데이터 : + (1) net + - 성남시 정자동 부근의 샘플 네트워크 + (2) inter_node + - 교차로번호와 노드id를 매칭시키는 테이블. + - parent/child 정보도 포함되어 있음 + - 컬럼 : inter_no, node_id, inter_type + (3) inter_info + - 교차로 정보. 여기에서는 위도와 경도가 쓰임. + - 컬럼 : inter_no, inter_name, inter_lat, inter_lon, group_no, main_phase_no + + 진입엣지id, 진출엣지id를 얻는 과정 : + - match5 = match4.copy()의 각 열을 순회하면서 아래 과정을 반복함. + * 진입에 대해서만 서술하겠지만 진출도 마찬가지로 설명될 수 있음 + - 해당 행의 교차로정보로부터 노드ID를 얻어내고, 해당 노드에 대한 모든 진출엣지id를 inc_edges에 저장. + * inc_edge(진입엣지) : incoming edge, out_edge(진출엣지) : outgoing_edge + - inc_edges의 모든 진입엣지에 대하여 진입방향(inc_dires, 2차원 단위벡터)을 얻어냄. + - 해당 행의 진입각으로부터 그에 대응되는 진입각방향(단위벡터)를 얻어냄. + - 주어진 진입각방향에 대하여 내적이 가장 작은 진입방향에 대한 진입엣지를 inc_edge_id로 지정함. + ''' + + # parent node만 가져옴. + inter_node1 = self.inter_node[self.inter_node.inter_type == 'parent'].drop('inter_type', axis=1) + inter_info1 = self.inter_info[['inter_no', 'inter_lat', 'inter_lon']] + inter = pd.merge(inter_node1, inter_info1, how='left', left_on=['inter_no'], + right_on=['inter_no']).drop_duplicates() + + self.inter2node = dict(zip(inter['inter_no'], inter['node_id'])) + + self.match5 = self.match4.copy() + # 진입진출ID 매칭 + for index, row in self.match5.iterrows(): + node_id = self.inter2node[row.inter_no] + node = self.net.getNode(node_id) + # 교차로의 모든 (from / to) edges + inc_edges = [edge for edge in node.getIncoming() if edge.getFunction() == ''] # incoming edges + out_edges = [edge for edge in node.getOutgoing() if edge.getFunction() == ''] # outgoing edges + # 교차로의 모든 (from / to) directions + inc_dirs = [] + for inc_edge in inc_edges: + start = inc_edge.getShape()[-2] + end = inc_edge.getShape()[-1] + inc_dir = np.array(end) - np.array(start) + inc_dir = inc_dir / (inc_dir ** 2).sum() ** 0.5 + inc_dirs.append(inc_dir) + out_dirs = [] + for out_edge in out_edges: + start = out_edge.getShape()[0] + end = out_edge.getShape()[1] + out_dir = np.array(end) - np.array(start) + out_dir = out_dir / (out_dir ** 2).sum() ** 0.5 + out_dirs.append(out_dir) + # 진입각, 진출각 불러오기 + if not pd.isna(row.inc_angle): + inc_angle = int(row.inc_angle) + out_angle = int(row.out_angle) + # 방위각을 일반각으로 가공, 라디안 변환, 단위벡터로 변환 + inc_angle = (-90 - inc_angle) % 360 + inc_angle = inc_angle * np.pi / 180. + inc_dir_true = np.array([np.cos(inc_angle), np.sin(inc_angle)]) + out_angle = (90 - out_angle) % 360 + out_angle = out_angle * np.pi / 180. + out_dir_true = np.array([np.cos(out_angle), np.sin(out_angle)]) + # 매칭 엣지 반환 + inc_index = np.array([np.dot(inc_dir, inc_dir_true) for inc_dir in inc_dirs]).argmax() + out_index = np.array([np.dot(out_dir, out_dir_true) for out_dir in out_dirs]).argmax() + inc_edge_id = inc_edges[inc_index].getID() + out_edge_id = out_edges[out_index].getID() + self.match5.at[index, 'inc_edge'] = inc_edge_id + self.match5.at[index, 'out_edge'] = out_edge_id + self.match5['node_id'] = self.match5['inter_no'].map(self.inter2node) + self.match5 = self.match5.sort_values(by=['inter_no','phase_no','ring_type']).reset_index(drop=True) + + # 2-1-6 + def make_match6(self): + ''' + 진입엣지id, 진출엣지id, 노드id를 추가함 (부교차로). + - match6의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id + + 사용된 데이터 : + (1) inter_node + - 교차로번호와 노드id를 매칭시키는 테이블. + - parent/child 정보도 포함되어 있음 + - 컬럼 : inter_no, node_id, inter_type + (2) uturn (유턴정보) + - 컬럼 : parent_id, child_id, direction, condition, inc_edge, out_edge + - parent_id, child_id : 주교차로id, 유턴교차로id + - direction : 주교차로에 대한 유턴노드의 상대적인 위치(방향) + - condition : 좌회전시, 직진시, 직좌시, 보행신호시 중 하나 + - inc_edge, out_edge : 유턴에 대한 진입진출엣지 + (3) coord (연동교차로정보) + - 컬럼 : parent_id, child_id, phase_no, ring_type, inc_edge, out_edge + - parent_id, child_id : 주교차로id, 연동교차로id + - 나머지 컬럼 : 각 (현시, 링)별 진입진출엣지 + + 설명 : + - match5는 주교차로에 대해서만 진입엣지id, 진출엣지id, 노드id를 추가했었음. + 여기에서 uturn, coord를 사용해서 부교차로들(유턴교차로, 연동교차로)에 대해서도 해당 값들을 부여함. + 유턴교차로 : + - directions를 정북기준 시계방향의 8방위로 정함. + - 이를 통해 진입방향이 주어진 경우에 좌회전, 직진, 보행 등에 대한 (진입방향, 진출방향)을 얻어낼 수 있음. + - 예) 진입방향(direction)이 '북'일 때, + - 직진 : (북, 남) + * 남 : directions[(ind + 4) % len(directions)] + - 좌회전 : (북, 동) + * 동 : directions[(ind + 2) % len(directions)] + - 보행 : (서, 동) + * 서 : directions[(ind - 2) % len(directions)] + - uturn의 각 행을 순회하면서 아래 과정을 반복함 + - match5에서 parent_id에 해당하는 행들을 가져옴(cmatch). + - condition 별로 진입방향, 진출방향A, 진출방향B 정함. + - 상술한 directions를 활용하여 정함. + - (진입방향, 진출방향A, 진출방향B)을 고려하여 (현시, 링) 별로 진입엣지id, 진출엣지id를 정함. + - ex) cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + - 순회하면서 만든 cmatch를 cmatchs라는 리스트에 저장함. + + 연동교차로 : + - 연동교차로의 경우 coord에 (현시, 링)별 진입엣지ID, 진출엣지ID가 명시되어 있음. + - 'inc_dir', 'out_dir', 'inc_angle','out_angle'와 같은 열들은 np.nan을 지정해놓음. + - 이 열들은, 사실상 다음 스텝부터는 사용되지 않는 열들이기 때문에 np.nan으로 지정해놓아도 문제없음. + + match6 : + - 이렇게 얻은 match5, cmatchs, coord를 모두 pd.concat하여 match6을 얻어냄. + ''' + + self.node2inter = dict(zip(self.inter_node['node_id'], self.inter_node['inter_no'])) + + child_ids = self.inter_node[self.inter_node.inter_type=='child'].node_id.unique() + ch2pa = {} # child to parent + for child_id in child_ids: + parent_no = self.inter_node[self.inter_node.node_id==child_id].inter_no.iloc[0] + sub_inter_node = self.inter_node[self.inter_node.inter_no==parent_no] + ch2pa[child_id] = sub_inter_node[sub_inter_node.inter_type=='parent'].iloc[0].node_id + directions = ['북', '북동', '동', '남동', '남', '남서', '서', '북서'] # 정북기준 시계방향으로 8방향 + + # 각 uturn node에 대하여 (inc_edge_id, out_edge_id) 부여 + cmatches = [] + for _, row in self.uturn.iterrows(): + child_id = row.child_id + parent_id = row.parent_id + direction = row.direction + condition = row.condition + inc_edge_id = row.inc_edge + out_edge_id = row.out_edge + # match5에서 parent_id에 해당하는 행들을 가져옴 + cmatch = self.match5.copy()[self.match5.node_id==parent_id] # match dataframe for a child node + cmatch = cmatch.sort_values(by=['phase_no', 'ring_type']).reset_index(drop=True) + cmatch['node_id'] = child_id + cmatch[['inc_edge', 'out_edge']] = np.nan + + # condition 별로 inc_dire, out_dire_A, out_dire_B를 정함 + ind = directions.index(direction) + if condition == "좌회전시": + inc_dire = direction + out_dire_A = out_dire_B = directions[(ind + 2) % len(directions)] + elif condition == "직진시": + inc_dire = direction + out_dire_A = out_dire_B = directions[(ind + 4) % len(directions)] + elif condition == "보행신호시": + inc_dire = directions[(ind + 2) % len(directions)] + out_dire_A = directions[(ind - 2) % len(directions)] + out_dire_B = directions[(ind - 2) % len(directions)] + + # (inc_dire, out_dire_A, out_dire_B) 별로 inc_edge_id, out_edge_id를 정함 + if condition == '보행신호시': + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + # 이동류번호가 17(보행신호)이면서 유턴노드방향으로 가는 신호가 없으면 (inc_edge_id, out_edge_id)를 부여한다. + cmatch.loc[(cmatch.move_no==17) & (cmatch.out_dir!=direction), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + else: # '직진시', '좌회전시' + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + # 유턴신호의 이동류번호를 19로 부여한다. + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), 'move_no'] = 19 + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), 'move_no'] = 19 + cmatches.append(cmatch) + + # 각 coordination node에 대하여 (inc_edge_id, out_edge_id) 부여 + self.coord['inter_no'] = self.coord['parent_id'].map(self.node2inter) + self.coord = self.coord.rename(columns={'child_id':'node_id'}) + self.coord[['inc_dir', 'out_dir', 'inc_angle','out_angle']] = np.nan + self.coord['move_no'] = 20 + self.coord = self.coord[['inter_no', 'phase_no', 'ring_type', 'move_no', 'inc_dir', 'out_dir', 'inc_angle','out_angle', 'inc_edge', 'out_edge', 'node_id']] + + # display(coord) + cmatches = pd.concat(cmatches) + self.match6 = pd.concat([self.match5, cmatches, self.coord]).drop_duplicates().sort_values(by=['inter_no', 'node_id', 'phase_no', 'ring_type']) + self.match6.to_csv(os.path.join(self.path_root, 'Intermediates', 'match6.csv')) + + # 2-1-7 + def make_matching(self): + ''' + 이동류 매칭 : 각 교차로에 대하여, 가능한 모든 이동류 (1~18, 21)에 대한 진입·진출엣지ID를 지정한다. + 모든 이동류에 대해 지정하므로, 시차제시 이전과 다른 이동류가 등장하더라도 항상 진입·진출 엣지 ID를 지정할 수 있다. + - matching의 컬럼 : inter_no, move_no, inc_dir, out_dir, inc_edge, out_edge, node_id + + 설명 : + - 필요한 리스트, 딕셔너리 등을 정의 + (1) 가능한 (진입방향, 진출방향) 목록 [리스트] + (2) 각 교차로별 방향 목록 : pdires (possible directions) [딕셔너리] + (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) [딕셔너리] + (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) [딕셔너리] + (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) [딕셔너리] + - matching은 빈 리스트로 지정. + - 모든 노드id에 대하여 다음 과정을 반복 + - 해당 노드id에 대한 모든 가능한 (진입방향, 진출방향)에 대하여 다음 과정을 반복 + - (노드id, 진입방향)으로부터 진입엣지id를 얻어냄. 마찬가지로 진출엣지id도 얻어냄 + - 얻어낸 정보를 바탕으로 한 행(new_row)을 만들고 이것을 matching에 append + ''' + + self.match7 = self.match6.copy() + self.match7 = self.match7[['inter_no', 'move_no', 'inc_dir', 'out_dir', 'inc_edge', 'out_edge', 'node_id']] + + parent_ids = sorted(self.inter_node[self.inter_node.inter_type=='parent'].node_id.unique()) + child_ids = sorted(self.inter_node[self.inter_node.inter_type=='child'].node_id.unique()) + + # (1) 가능한 (진입방향, 진출방향) 목록 + flows = self.nema.dropna().apply(lambda row: (row['inc_dir'], row['out_dir']), axis=1).tolist() + # (2) 각 교차로별 방향 목록 : pdires (possible directions) + pdires = {} + for node_id in parent_ids: + dires = self.match7[self.match7.node_id == node_id][['inc_dir','out_dir']].values.flatten() + dires = {dire for dire in dires if type(dire)==str} + pdires[node_id] = dires + # (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) + inc2id = {} + for node_id in parent_ids: + for inc_dir in pdires[node_id]: + df = self.match7[(self.match7.node_id==node_id) & (self.match7.inc_dir==inc_dir)] + inc2id[(node_id, inc_dir)] = df.inc_edge.iloc[0] + # (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) + out2id = {} + for node_id in parent_ids: + for out_dir in pdires[node_id]: + df = self.match7[(self.match7.node_id==node_id) & (self.match7.out_dir==out_dir)] + out2id[(node_id, out_dir)] = df.out_edge.iloc[0] + # (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) + pflow = {} + for node_id in parent_ids: + pflow[node_id] = [flow for flow in flows if set(flow).issubset(pdires[node_id])] + # (6) 가능한 이동류에 대하여 진입id, 진출id 배정 : matching + # node2inter = dict(zip(self.match7['node_id'], self.match7['inter_no'])) + dires_right = ['북', '서', '남', '동', '북'] # ex (북, 서), (서, 남) 등은 우회전 flow + self.matching = [] + for node_id in parent_ids: + inter_no = self.node2inter[node_id] + # 좌회전과 직진(1 ~ 16) + for (inc_dir, out_dir) in pflow[node_id]: + move_no = self.nema[(self.nema.inc_dir==inc_dir) & (self.nema.out_dir==out_dir)].move_no.iloc[0] inc_edge = inc2id[(node_id, inc_dir)] out_edge = out2id[(node_id, out_dir)] - new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[21], + new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[move_no], 'inc_dir':[inc_dir], 'out_dir':[out_dir], 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) - matching.append(new_row) - matching.append(match7[match7.node_id.isin(child_ids)]) - matching = pd.concat(matching) - matching = matching.dropna().sort_values(by=['inter_no', 'node_id', 'move_no']).reset_index(drop=True) - matching['move_no'] = matching['move_no'].astype(int) - matching.to_csv(os.path.join(path_root, 'Intermediates', 'matching.csv')) - return matching - -def make_movements(path_root): - movements_path = os.path.join(path_root, 'Intermediates', 'movement') - movements_list = [pd.read_csv(os.path.join(movements_path, file), index_col=0) for file in tqdm(os.listdir(movements_path), desc='이동류정보 불러오는 중 : movements')] - movements = pd.concat(movements_list) - movements = movements.drop(columns=['start_unix']) - movements = movements.drop_duplicates() - movements = movements.sort_values(by=['inter_no', 'phas_A', 'phas_B']) - movements = movements.reset_index(drop=True) - movements.to_csv(os.path.join(path_root, 'Intermediates', 'movements.csv')) - return movements - -# node2num_cycles : A dictionary that maps a node_id to the number of cycles -def get_node2num_cycles(plan, inter_node, path_root): - node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no'])) - node_ids = sorted(inter_node.node_id.unique()) - - Aplan = plan.copy()[['inter_no'] + [f'dura_A{j}' for j in range(1,9)] + ['cycle']] - grouped = Aplan.groupby('inter_no') - df = grouped.agg({'cycle': 'min'}).reset_index() - df = df.rename(columns={'cycle': 'min_cycle'}) - df['num_cycle'] = 300 // df['min_cycle'] + 2 - inter2num_cycles = dict(zip(df['inter_no'], df['num_cycle'])) - node2numcycles = {node_id : inter2num_cycles[node2inter[node_id]] for node_id in node_ids} - with open(os.path.join('Intermediates','node2numcycles.json'), 'w') as file: - json.dump(node2numcycles, file, indent=4) - return node2numcycles - -def main(): - path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - inter_info = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'inter_info.csv')) - check_inter_info(inter_info) - angle = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'angle.csv'), dtype = {f'angle_{alph}{j}':'str' for alph in ['A', 'B'] for j in range(1,9)}) - plan = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'plan.csv')) - inter_node = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'inter_node.csv')) - uturn = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'child_uturn.csv')) - coord = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'child_coord.csv')) - nema = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'nema.csv'), encoding='cp949') - - net = sumolib.net.readNet(os.path.join(path_root, 'Data', 'networks', 'sn.net.xml')) - - match1 = make_match1(path_root) - match2 = make_match2(match1) - match3 = make_match3(match2, nema) - match4 = make_match4(match3, angle) - match5 = make_match5(match4, net, inter_node, inter_info) - match6 = make_match6(match5, inter_node, uturn, coord, path_root) - matching = make_matching(match6, inter_node, nema, path_root) - movements = make_movements(path_root) - node2num_cycles = get_node2num_cycles(plan, inter_node, path_root) + self.matching.append(new_row) + # 보행신호(17), 전적색(18) + new_row = pd.DataFrame({'inter_no':[inter_no] * 2, 'move_no':[17, 18], + 'inc_dir':[None]*2, 'out_dir':[None]*2, + 'inc_edge':[None]*2, 'out_edge':[None]*2, 'node_id':[node_id]*2}) + self.matching.append(new_row) + # 신호우회전(21) + for d in range(len(dires_right)-1): + inc_dir = dires_right[d] + out_dir = dires_right[d+1] + if {inc_dir, out_dir}.issubset(pdires[node_id]): + inc_edge = inc2id[(node_id, inc_dir)] + out_edge = out2id[(node_id, out_dir)] + new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[21], + 'inc_dir':[inc_dir], 'out_dir':[out_dir], + 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) + self.matching.append(new_row) + self.matching.append(self.match7[self.match7.node_id.isin(child_ids)]) + self.matching = pd.concat(self.matching) + self.matching = self.matching.dropna().sort_values(by=['inter_no', 'node_id', 'move_no']).reset_index(drop=True) + self.matching['move_no'] = self.matching['move_no'].astype(int) + self.matching.to_csv(os.path.join(self.path_root, 'Intermediates', 'matching.csv')) + + # 2-2 + def get_movements(self): + movements_path = os.path.join(self.path_root, 'Intermediates', 'movement') + movements_list = [pd.read_csv(os.path.join(movements_path, file), index_col=0) for file in tqdm(os.listdir(movements_path), desc='이동류정보 불러오는 중 : movements')] + movements = pd.concat(movements_list) + movements = movements.drop(columns=['start_unix']) + movements = movements.drop_duplicates() + movements = movements.sort_values(by=['inter_no', 'phas_A', 'phas_B']) + movements = movements.reset_index(drop=True) + movements.to_csv(os.path.join(self.path_root, 'Intermediates', 'movements.csv')) + print("2-2. movements를 생성했습니다.") + + # 2-3 node2num_cycles : A dictionary that maps a node_id to the number of cycles + def get_node2num_cycles(self): + # node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no'])) + self.node_ids = sorted(self.inter_node.node_id.unique()) + + Aplan = self.plan.copy()[['inter_no'] + [f'dura_A{j}' for j in range(1,9)] + ['cycle']] + grouped = Aplan.groupby('inter_no') + df = grouped.agg({'cycle': 'min'}).reset_index() + df = df.rename(columns={'cycle': 'min_cycle'}) + df['num_cycle'] = 300 // df['min_cycle'] + 2 + inter2num_cycles = dict(zip(df['inter_no'], df['num_cycle'])) + node2num_cycles = {node_id : inter2num_cycles[self.node2inter[node_id]] for node_id in self.node_ids} + with open(os.path.join('Intermediates','node2num_cycles.json'), 'w') as file: + json.dump(node2num_cycles, file, indent=4) + print("2-3. node2num_cycles.json를 저장했습니다.") + + # 3. 이슈사항 저장 + def write_issues(self): + print('3. 이슈사항을 저장합니다.') + path_issues = os.path.join(self.path_root, "Results", "issues_preprocess_daily.txt") + with open(path_issues, "w", encoding="utf-8") as file: + for item in self.issues: + file.write(item + "\n") + if self.issues: + print("데이터 처리 중 발생한 특이사항은 다음과 같습니다. :") + for review in self.issues: + print(review) + + def main(self): + # 1. 데이터 불러오기 + self.load_data() + # 2. 중간산출물 만들기 + self.get_intermediates() + # 3. 이슈사항 저장 + self.write_issues() if __name__ == '__main__': - main() + self = DailyPreprocessor() + self.main() \ No newline at end of file diff --git a/Script/preprocess_daily_0.py b/Script/preprocess_daily_0.py new file mode 100644 index 000000000..f0de51881 --- /dev/null +++ b/Script/preprocess_daily_0.py @@ -0,0 +1,431 @@ +import pandas as pd +import numpy as np +import os +import json +import sumolib +from tqdm import tqdm + +def check_inter_info(inter_info): + print(inter_info) + print('check') + +def make_match1(path_root): + ''' + 신호 DB에는 매 초마다 이동류정보가 업데이트 된다. 그리고 이 이동류정보를 매 5초마다 불러와서 사용하게 된다. + '../Data/tables/move/'에는 5초마다의 이동류정보가 저장되어 있다. + + return : 통합된 이동류정보 + - 모든 inter_no(교차로번호)에 대한 A, B링 현시별 이동류정보 + + match1을 만드는 데 시간이 소요되므로 한 번 만들어서 저장해두고 저장해둔 것을 쓴다. + ''' + # [이동류번호] 불러오기 (약 1분의 소요시간) + path_move = os.path.join(path_root, 'Data', 'tables', 'move') + csv_moves = os.listdir(path_move) + moves = [pd.read_csv(os.path.join(path_move, csv_move), index_col=0) for csv_move in tqdm(csv_moves, desc='이동류정보 불러오는 중 : match1')] + match1 = pd.concat(moves).drop_duplicates().sort_values(by=['inter_no','phas_A','phas_B']).reset_index(drop=True) + match1.to_csv(os.path.join(path_root, 'Intermediates', 'match1.csv')) + return match1 + +def make_match2(match1): + ''' + match1을 계층화함. + - match1의 컬럼 : inter_no, phas_A, phas_B, move_A, move_B + - match2의 컬럼 : inter_no, phase_no, ring_type, move_no + ''' + # 계층화 (inter_no, phas_A, phas_B, move_A, move_B) -> ('inter_no', 'phase_no', 'ring_type', 'move_no') + matchA = match1[['inter_no', 'phas_A', 'move_A']].copy() + matchA.columns = ['inter_no', 'phase_no', 'move_no'] + matchA['ring_type'] = 'A' + matchB = match1[['inter_no', 'phas_B', 'move_B']].copy() + matchB.columns = ['inter_no', 'phase_no', 'move_no'] + matchB['ring_type'] = 'B' + match2 = pd.concat([matchA, matchB]).drop_duplicates() + match2 = match2[['inter_no', 'phase_no', 'ring_type', 'move_no']] + match2 = match2.sort_values(by=list(match2.columns)) + return match2 + +def make_match3(match2, nema): + ''' + 각 movement들에 방향(진입방향, 진출방향)을 매칭시켜 추가함. + - match2의 컬럼 : inter_no, phase_no, ring_type, move_no + - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir + + nema : + - 컬럼 : move_no, inc_dir, out_dir + - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 + - 이동류번호 : 1 ~ 16, 17, 18, 21 + - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 + ''' + # nema 정보 불러오기 및 병합 + match3 = pd.merge(match2, nema, how='left', on='move_no').drop_duplicates() + return match3 + +def make_match4(match3, angle): + ''' + 방위각 정보를 매칭시켜 추가함. + - match3의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir + - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle + + angle_original : + - 컬럼 : inter_no, angle_Aj, angle_Bj (j : 1 ~ 8) + - 모든 종류의 이동류번호에 대하여 진입방향과 진출방향을 매칭시키는 테이블 + - 이동류번호 : 1 ~ 16, 17, 18, 21 + - 진입, 진출방향(8방위) : 동, 서, 남, 북, 북동, 북서, 남동, 남서 + ''' + + # 계층화 + angles = [] + for i, row in angle.iterrows(): + angle_codes = row[[f'angle_{alph}{j}' for alph in ['A', 'B'] for j in range(1,9)]] + new = pd.DataFrame({'inter_no':[row.inter_no] * 16, 'phase_no':list(range(1, 9))*2, 'ring_type':['A'] * 8 + ['B'] * 8, 'angle_code':angle_codes.to_list()}) + angles.append(new) + angles = pd.concat(angles) + angles = angles.dropna().reset_index(drop=True) + + # 병합 + six_chars = angles.angle_code.apply(lambda x:len(x)==6) + angles.loc[six_chars,'inc_angle'] = angles.angle_code.apply(lambda x:x[:3]) + angles.loc[six_chars,'out_angle'] = angles.angle_code.apply(lambda x:x[3:]) + angles = angles.drop('angle_code', axis=1) + match4 = pd.merge(match3, angles, how='left', left_on=['inter_no', 'phase_no', 'ring_type'], + right_on=['inter_no', 'phase_no', 'ring_type']).drop_duplicates() + return match4 + +def make_match5(match4, net, inter_node, inter_info): + ''' + 진입엣지id, 진출엣지id, 노드id를 추가함 (주교차로). + - match4의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle + - match5의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id + + 사용된 데이터 : + (1) net + - 성남시 정자동 부근의 샘플 네트워크 + (2) inter_node + - 교차로번호와 노드id를 매칭시키는 테이블. + - parent/child 정보도 포함되어 있음 + - 컬럼 : inter_no, node_id, inter_type + (3) inter_info + - 교차로 정보. 여기에서는 위도와 경도가 쓰임. + - 컬럼 : inter_no, inter_name, inter_lat, inter_lon, group_no, main_phase_no + + 진입엣지id, 진출엣지id를 얻는 과정 : + - match5 = match4.copy()의 각 열을 순회하면서 아래 과정을 반복함. + * 진입에 대해서만 서술하겠지만 진출도 마찬가지로 설명될 수 있음 + - 해당 행의 교차로정보로부터 노드ID를 얻어내고, 해당 노드에 대한 모든 진출엣지id를 inc_edges에 저장. + * inc_edge(진입엣지) : incoming edge, out_edge(진출엣지) : outgoing_edge + - inc_edges의 모든 진입엣지에 대하여 진입방향(inc_dires, 2차원 단위벡터)을 얻어냄. + - 해당 행의 진입각으로부터 그에 대응되는 진입각방향(단위벡터)를 얻어냄. + - 주어진 진입각방향에 대하여 내적이 가장 작은 진입방향에 대한 진입엣지를 inc_edge_id로 지정함. + ''' + + # parent node만 가져옴. + inter_node1 = inter_node[inter_node.inter_type == 'parent'].drop('inter_type', axis=1) + inter_info1 = inter_info[['inter_no', 'inter_lat', 'inter_lon']] + inter = pd.merge(inter_node1, inter_info1, how='left', left_on=['inter_no'], + right_on=['inter_no']).drop_duplicates() + + inter2node = dict(zip(inter['inter_no'], inter['node_id'])) + + match5 = match4.copy() + # 진입진출ID 매칭 + for index, row in match5.iterrows(): + node_id = inter2node[row.inter_no] + node = net.getNode(node_id) + # 교차로의 모든 (from / to) edges + inc_edges = [edge for edge in node.getIncoming() if edge.getFunction() == ''] # incoming edges + out_edges = [edge for edge in node.getOutgoing() if edge.getFunction() == ''] # outgoing edges + # 교차로의 모든 (from / to) directions + inc_dirs = [] + for inc_edge in inc_edges: + start = inc_edge.getShape()[-2] + end = inc_edge.getShape()[-1] + inc_dir = np.array(end) - np.array(start) + inc_dir = inc_dir / (inc_dir ** 2).sum() ** 0.5 + inc_dirs.append(inc_dir) + out_dirs = [] + for out_edge in out_edges: + start = out_edge.getShape()[0] + end = out_edge.getShape()[1] + out_dir = np.array(end) - np.array(start) + out_dir = out_dir / (out_dir ** 2).sum() ** 0.5 + out_dirs.append(out_dir) + # 진입각, 진출각 불러오기 + if not pd.isna(row.inc_angle): + inc_angle = int(row.inc_angle) + out_angle = int(row.out_angle) + # 방위각을 일반각으로 가공, 라디안 변환, 단위벡터로 변환 + inc_angle = (-90 - inc_angle) % 360 + inc_angle = inc_angle * np.pi / 180. + inc_dir_true = np.array([np.cos(inc_angle), np.sin(inc_angle)]) + out_angle = (90 - out_angle) % 360 + out_angle = out_angle * np.pi / 180. + out_dir_true = np.array([np.cos(out_angle), np.sin(out_angle)]) + # 매칭 엣지 반환 + inc_index = np.array([np.dot(inc_dir, inc_dir_true) for inc_dir in inc_dirs]).argmax() + out_index = np.array([np.dot(out_dir, out_dir_true) for out_dir in out_dirs]).argmax() + inc_edge_id = inc_edges[inc_index].getID() + out_edge_id = out_edges[out_index].getID() + match5.at[index, 'inc_edge'] = inc_edge_id + match5.at[index, 'out_edge'] = out_edge_id + match5['node_id'] = match5['inter_no'].map(inter2node) + match5 = match5.sort_values(by=['inter_no','phase_no','ring_type']).reset_index(drop=True) + return match5 + +def make_match6(match5, inter_node, uturn, coord, path_root): + ''' + 진입엣지id, 진출엣지id, 노드id를 추가함 (부교차로). + - match6의 컬럼 : inter_no, phase_no, ring_type, move_no, inc_dir, out_dir, inc_angle, out_angle, inc_edge, out_edge, node_id + + 사용된 데이터 : + (1) inter_node + - 교차로번호와 노드id를 매칭시키는 테이블. + - parent/child 정보도 포함되어 있음 + - 컬럼 : inter_no, node_id, inter_type + (2) uturn (유턴정보) + - 컬럼 : parent_id, child_id, direction, condition, inc_edge, out_edge + - parent_id, child_id : 주교차로id, 유턴교차로id + - direction : 주교차로에 대한 유턴노드의 상대적인 위치(방향) + - condition : 좌회전시, 직진시, 직좌시, 보행신호시 중 하나 + - inc_edge, out_edge : 유턴에 대한 진입진출엣지 + (3) coord (연동교차로정보) + - 컬럼 : parent_id, child_id, phase_no, ring_type, inc_edge, out_edge + - parent_id, child_id : 주교차로id, 연동교차로id + - 나머지 컬럼 : 각 (현시, 링)별 진입진출엣지 + + 설명 : + - match5는 주교차로에 대해서만 진입엣지id, 진출엣지id, 노드id를 추가했었음. + 여기에서 uturn, coord를 사용해서 부교차로들(유턴교차로, 연동교차로)에 대해서도 해당 값들을 부여함. + 유턴교차로 : + - directions를 정북기준 시계방향의 8방위로 정함. + - 이를 통해 진입방향이 주어진 경우에 좌회전, 직진, 보행 등에 대한 (진입방향, 진출방향)을 얻어낼 수 있음. + - 예) 진입방향(direction)이 '북'일 때, + - 직진 : (북, 남) + * 남 : directions[(ind + 4) % len(directions)] + - 좌회전 : (북, 동) + * 동 : directions[(ind + 2) % len(directions)] + - 보행 : (서, 동) + * 서 : directions[(ind - 2) % len(directions)] + - uturn의 각 행을 순회하면서 아래 과정을 반복함 + - match5에서 parent_id에 해당하는 행들을 가져옴(cmatch). + - condition 별로 진입방향, 진출방향A, 진출방향B 정함. + - 상술한 directions를 활용하여 정함. + - (진입방향, 진출방향A, 진출방향B)을 고려하여 (현시, 링) 별로 진입엣지id, 진출엣지id를 정함. + - ex) cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + - 순회하면서 만든 cmatch를 cmatchs라는 리스트에 저장함. + + 연동교차로 : + - 연동교차로의 경우 coord에 (현시, 링)별 진입엣지ID, 진출엣지ID가 명시되어 있음. + - 'inc_dir', 'out_dir', 'inc_angle','out_angle'와 같은 열들은 np.nan을 지정해놓음. + - 이 열들은, 사실상 다음 스텝부터는 사용되지 않는 열들이기 때문에 np.nan으로 지정해놓아도 문제없음. + + match6 : + - 이렇게 얻은 match5, cmatchs, coord를 모두 pd.concat하여 match6을 얻어냄. + ''' + + node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no'])) + + child_ids = inter_node[inter_node.inter_type=='child'].node_id.unique() + ch2pa = {} # child to parent + for child_id in child_ids: + parent_no = inter_node[inter_node.node_id==child_id].inter_no.iloc[0] + sub_inter_node = inter_node[inter_node.inter_no==parent_no] + ch2pa[child_id] = sub_inter_node[sub_inter_node.inter_type=='parent'].iloc[0].node_id + directions = ['북', '북동', '동', '남동', '남', '남서', '서', '북서'] # 정북기준 시계방향으로 8방향 + + # 각 uturn node에 대하여 (inc_edge_id, out_edge_id) 부여 + cmatches = [] + for _, row in uturn.iterrows(): + child_id = row.child_id + parent_id = row.parent_id + direction = row.direction + condition = row.condition + inc_edge_id = row.inc_edge + out_edge_id = row.out_edge + # match5에서 parent_id에 해당하는 행들을 가져옴 + cmatch = match5.copy()[match5.node_id==parent_id] # match dataframe for a child node + cmatch = cmatch.sort_values(by=['phase_no', 'ring_type']).reset_index(drop=True) + cmatch['node_id'] = child_id + cmatch[['inc_edge', 'out_edge']] = np.nan + + # condition 별로 inc_dire, out_dire_A, out_dire_B를 정함 + ind = directions.index(direction) + if condition == "좌회전시": + inc_dire = direction + out_dire_A = out_dire_B = directions[(ind + 2) % len(directions)] + elif condition == "직진시": + inc_dire = direction + out_dire_A = out_dire_B = directions[(ind + 4) % len(directions)] + elif condition == "보행신호시": + inc_dire = directions[(ind + 2) % len(directions)] + out_dire_A = directions[(ind - 2) % len(directions)] + out_dire_B = directions[(ind - 2) % len(directions)] + + # (inc_dire, out_dire_A, out_dire_B) 별로 inc_edge_id, out_edge_id를 정함 + if condition == '보행신호시': + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + # 이동류번호가 17(보행신호)이면서 유턴노드방향으로 가는 신호가 없으면 (inc_edge_id, out_edge_id)를 부여한다. + cmatch.loc[(cmatch.move_no==17) & (cmatch.out_dir!=direction), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + else: # '직진시', '좌회전시' + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), ['inc_edge', 'out_edge']] = [inc_edge_id, out_edge_id] + # 유턴신호의 이동류번호를 19로 부여한다. + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_A), 'move_no'] = 19 + cmatch.loc[(cmatch.inc_dir==inc_dire) & (cmatch.out_dir==out_dire_B), 'move_no'] = 19 + cmatches.append(cmatch) + + # 각 coordination node에 대하여 (inc_edge_id, out_edge_id) 부여 + coord['inter_no'] = coord['parent_id'].map(node2inter) + coord = coord.rename(columns={'child_id':'node_id'}) + coord[['inc_dir', 'out_dir', 'inc_angle','out_angle']] = np.nan + coord['move_no'] = 20 + coord = coord[['inter_no', 'phase_no', 'ring_type', 'move_no', 'inc_dir', 'out_dir', 'inc_angle','out_angle', 'inc_edge', 'out_edge', 'node_id']] + + # display(coord) + cmatches = pd.concat(cmatches) + match6 = pd.concat([match5, cmatches, coord]).drop_duplicates().sort_values(by=['inter_no', 'node_id', 'phase_no', 'ring_type']) + match6.to_csv(os.path.join(path_root, 'Intermediates', 'match6.csv')) + return match6 + +def make_matching(match6, inter_node, nema, path_root): + ''' + 이동류 매칭 : 각 교차로에 대하여, 가능한 모든 이동류 (1~18, 21)에 대한 진입·진출엣지ID를 지정한다. + 모든 이동류에 대해 지정하므로, 시차제시 이전과 다른 이동류가 등장하더라도 항상 진입·진출 엣지 ID를 지정할 수 있다. + - matching의 컬럼 : inter_no, move_no, inc_dir, out_dir, inc_edge, out_edge, node_id + + 설명 : + - 필요한 리스트, 딕셔너리 등을 정의 + (1) 가능한 (진입방향, 진출방향) 목록 [리스트] + (2) 각 교차로별 방향 목록 : pdires (possible directions) [딕셔너리] + (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) [딕셔너리] + (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) [딕셔너리] + (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) [딕셔너리] + - matching은 빈 리스트로 지정. + - 모든 노드id에 대하여 다음 과정을 반복 + - 해당 노드id에 대한 모든 가능한 (진입방향, 진출방향)에 대하여 다음 과정을 반복 + - (노드id, 진입방향)으로부터 진입엣지id를 얻어냄. 마찬가지로 진출엣지id도 얻어냄 + - 얻어낸 정보를 바탕으로 한 행(new_row)을 만들고 이것을 matching에 append + ''' + + match7 = match6.copy() + match7 = match7[['inter_no', 'move_no', 'inc_dir', 'out_dir', 'inc_edge', 'out_edge', 'node_id']] + + parent_ids = sorted(inter_node[inter_node.inter_type=='parent'].node_id.unique()) + child_ids = sorted(inter_node[inter_node.inter_type=='child'].node_id.unique()) + + # (1) 가능한 (진입방향, 진출방향) 목록 + flows = nema.dropna().apply(lambda row: (row['inc_dir'], row['out_dir']), axis=1).tolist() + # (2) 각 교차로별 방향 목록 : pdires (possible directions) + pdires = {} + for node_id in parent_ids: + dires = match7[match7.node_id == node_id][['inc_dir','out_dir']].values.flatten() + dires = {dire for dire in dires if type(dire)==str} + pdires[node_id] = dires + # (3) 각 (교차로, 진입방향) 별 진입id 목록 : inc2id (incoming direction to incoming edge_id) + inc2id = {} + for node_id in parent_ids: + for inc_dir in pdires[node_id]: + df = match7[(match7.node_id==node_id) & (match7.inc_dir==inc_dir)] + inc2id[(node_id, inc_dir)] = df.inc_edge.iloc[0] + # (4) 각 (교차로, 진출방향) 별 진출id 목록 : out2id (outgoing direction to outgoing edge_id) + out2id = {} + for node_id in parent_ids: + for out_dir in pdires[node_id]: + df = match7[(match7.node_id==node_id) & (match7.out_dir==out_dir)] + out2id[(node_id, out_dir)] = df.out_edge.iloc[0] + # (5) 각 교차로별 가능한 (진입방향, 진출방향) 목록 : pflow (possible flows) + pflow = {} + for node_id in parent_ids: + pflow[node_id] = [flow for flow in flows if set(flow).issubset(pdires[node_id])] + # (6) 가능한 이동류에 대하여 진입id, 진출id 배정 : matching + node2inter = dict(zip(match7['node_id'], match7['inter_no'])) + dires_right = ['북', '서', '남', '동', '북'] # ex (북, 서), (서, 남) 등은 우회전 flow + matching = [] + for node_id in parent_ids: + inter_no = node2inter[node_id] + # 좌회전과 직진(1 ~ 16) + for (inc_dir, out_dir) in pflow[node_id]: + move_no = nema[(nema.inc_dir==inc_dir) & (nema.out_dir==out_dir)].move_no.iloc[0] + inc_edge = inc2id[(node_id, inc_dir)] + out_edge = out2id[(node_id, out_dir)] + new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[move_no], + 'inc_dir':[inc_dir], 'out_dir':[out_dir], + 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) + matching.append(new_row) + # 보행신호(17), 전적색(18) + new_row = pd.DataFrame({'inter_no':[inter_no] * 2, 'move_no':[17, 18], + 'inc_dir':[None]*2, 'out_dir':[None]*2, + 'inc_edge':[None]*2, 'out_edge':[None]*2, 'node_id':[node_id]*2}) + matching.append(new_row) + # 신호우회전(21) + for d in range(len(dires_right)-1): + inc_dir = dires_right[d] + out_dir = dires_right[d+1] + if {inc_dir, out_dir}.issubset(pdires[node_id]): + inc_edge = inc2id[(node_id, inc_dir)] + out_edge = out2id[(node_id, out_dir)] + new_row = pd.DataFrame({'inter_no':[inter_no], 'move_no':[21], + 'inc_dir':[inc_dir], 'out_dir':[out_dir], + 'inc_edge':[inc_edge], 'out_edge':[out_edge], 'node_id':[node_id]}) + matching.append(new_row) + matching.append(match7[match7.node_id.isin(child_ids)]) + matching = pd.concat(matching) + matching = matching.dropna().sort_values(by=['inter_no', 'node_id', 'move_no']).reset_index(drop=True) + matching['move_no'] = matching['move_no'].astype(int) + matching.to_csv(os.path.join(path_root, 'Intermediates', 'matching.csv')) + return matching + +def make_movements(path_root): + movements_path = os.path.join(path_root, 'Intermediates', 'movement') + movements_list = [pd.read_csv(os.path.join(movements_path, file), index_col=0) for file in tqdm(os.listdir(movements_path), desc='이동류정보 불러오는 중 : movements')] + movements = pd.concat(movements_list) + movements = movements.drop(columns=['start_unix']) + movements = movements.drop_duplicates() + movements = movements.sort_values(by=['inter_no', 'phas_A', 'phas_B']) + movements = movements.reset_index(drop=True) + movements.to_csv(os.path.join(path_root, 'Intermediates', 'movements.csv')) + return movements + +# node2num_cycles : A dictionary that maps a node_id to the number of cycles +def get_node2num_cycles(plan, inter_node, path_root): + node2inter = dict(zip(inter_node['node_id'], inter_node['inter_no'])) + node_ids = sorted(inter_node.node_id.unique()) + + Aplan = plan.copy()[['inter_no'] + [f'dura_A{j}' for j in range(1,9)] + ['cycle']] + grouped = Aplan.groupby('inter_no') + df = grouped.agg({'cycle': 'min'}).reset_index() + df = df.rename(columns={'cycle': 'min_cycle'}) + df['num_cycle'] = 300 // df['min_cycle'] + 2 + inter2num_cycles = dict(zip(df['inter_no'], df['num_cycle'])) + node2numcycles = {node_id : inter2num_cycles[node2inter[node_id]] for node_id in node_ids} + with open(os.path.join('Intermediates','node2numcycles.json'), 'w') as file: + json.dump(node2numcycles, file, indent=4) + return node2numcycles + +def main(): + path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + inter_info = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'inter_info.csv')) + check_inter_info(inter_info) + angle = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'angle.csv'), dtype = {f'angle_{alph}{j}':'str' for alph in ['A', 'B'] for j in range(1,9)}) + plan = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'plan.csv')) + inter_node = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'inter_node.csv')) + uturn = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'child_uturn.csv')) + coord = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'child_coord.csv')) + nema = pd.read_csv(os.path.join(path_root, 'Data', 'tables', 'nema.csv'), encoding='cp949') + + net = sumolib.net.readNet(os.path.join(path_root, 'Data', 'networks', 'sn.net.xml')) + + match1 = make_match1(path_root) + match2 = make_match2(match1) + match3 = make_match3(match2, nema) + match4 = make_match4(match3, angle) + match5 = make_match5(match4, net, inter_node, inter_info) + match6 = make_match6(match5, inter_node, uturn, coord, path_root) + matching = make_matching(match6, inter_node, nema, path_root) + movements = make_movements(path_root) + node2num_cycles = get_node2num_cycles(plan, inter_node, path_root) + +if __name__ == '__main__': + main() diff --git a/state_300.00.xml.gz b/state_300.00.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..0ed20c2e70a655c832a567371108db04db8b5b86 GIT binary patch literal 3780 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 literal 0 HcmV?d00001