DavMelchi commited on
Commit
1c0596c
·
1 Parent(s): de54a83

Add CIQ 4G Generator with LNBTS/LNCEL sheet generation, implement site parsing from CIQ brut Excel with eNodeB/cell name extraction, band detection from cell names, sector ID mapping, DL/UL EARFCN conversion, MIMO mode assignment per band, and multi-band block configuration builder with TAC/PCI/RSI parameters and Excel export

Browse files
Files changed (3) hide show
  1. app.py +1 -0
  2. apps/ciq_4g_generator.py +66 -0
  3. queries/process_ciq_4g.py +230 -0
app.py CHANGED
@@ -120,6 +120,7 @@ if check_password():
120
  ),
121
  st.Page("apps/ciq_2g_generator.py", title="🧾 CIQ 2G Generator"),
122
  st.Page("apps/ciq_3g_generator.py", title="🧾 CIQ 3G Generator"),
 
123
  st.Page("apps/core_dump_page.py", title="📠Parse dump core"),
124
  st.Page("apps/gps_converter.py", title="🧭GPS Converter"),
125
  st.Page("apps/distance.py", title="🛰Distance Calculator"),
 
120
  ),
121
  st.Page("apps/ciq_2g_generator.py", title="🧾 CIQ 2G Generator"),
122
  st.Page("apps/ciq_3g_generator.py", title="🧾 CIQ 3G Generator"),
123
+ st.Page("apps/ciq_4g_generator.py", title="🧾 CIQ 4G Generator"),
124
  st.Page("apps/core_dump_page.py", title="📠Parse dump core"),
125
  st.Page("apps/gps_converter.py", title="🧭GPS Converter"),
126
  st.Page("apps/distance.py", title="🛰Distance Calculator"),
apps/ciq_4g_generator.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import streamlit as st
3
+
4
+ from queries.process_ciq_4g import generate_ciq_4g_excel
5
+
6
+ st.title("CIQ 4G Generator")
7
+
8
+ ciq_file = st.file_uploader(
9
+ "Upload CIQ brut 4G (Excel)", type=["xlsx", "xls"], key="ciq4g_ciq"
10
+ )
11
+
12
+ col1, col2 = st.columns(2)
13
+ with col1:
14
+ year_suffix = st.text_input("Year suffix", value="25", key="ciq4g_year")
15
+ with col2:
16
+ bands = st.text_input(
17
+ "Bands string",
18
+ value="G9G18U9U21L8L18L26",
19
+ key="ciq4g_bands",
20
+ )
21
+
22
+ col3, col4 = st.columns(2)
23
+ with col3:
24
+ mcc = st.number_input("MCC", value=610, step=1, min_value=0, key="ciq4g_mcc")
25
+ with col4:
26
+ mnc = st.number_input("MNC", value=2, step=1, min_value=0, key="ciq4g_mnc")
27
+
28
+ if ciq_file is None:
29
+ st.info("Upload CIQ brut 4G Excel to generate CIQ 4G export.")
30
+ st.stop()
31
+
32
+ if st.button("Generate", type="primary"):
33
+ try:
34
+ with st.spinner("Generating CIQ 4G..."):
35
+ sheets, excel_bytes = generate_ciq_4g_excel(
36
+ ciq_file,
37
+ year_suffix=year_suffix.strip(),
38
+ bands=bands.strip(),
39
+ mcc=int(mcc),
40
+ mnc=int(mnc),
41
+ )
42
+ st.session_state["ciq4g_sheets"] = sheets
43
+ st.session_state["ciq4g_excel_bytes"] = excel_bytes
44
+ st.success("CIQ 4G generated")
45
+ except Exception as e:
46
+ st.error(f"Error: {e}")
47
+
48
+ sheets = st.session_state.get("ciq4g_sheets")
49
+ excel_bytes = st.session_state.get("ciq4g_excel_bytes")
50
+
51
+ if sheets:
52
+ tab_names = list(sheets.keys())
53
+ tabs = st.tabs(tab_names)
54
+ for t, name in zip(tabs, tab_names):
55
+ with t:
56
+ df: pd.DataFrame = sheets[name]
57
+ st.dataframe(df, use_container_width=True)
58
+
59
+ if excel_bytes:
60
+ st.download_button(
61
+ label="Download CIQ 4G Excel",
62
+ data=excel_bytes,
63
+ file_name="CIQ_4G.xlsx",
64
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
65
+ type="primary",
66
+ )
queries/process_ciq_4g.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import re
3
+ from typing import Optional
4
+
5
+ import pandas as pd
6
+
7
+
8
+ def _parse_int(value: object) -> Optional[int]:
9
+ v = pd.to_numeric(value, errors="coerce")
10
+ if pd.isna(v):
11
+ return None
12
+ return int(v)
13
+
14
+
15
+ def _strip_suffix(value: str, suffix: str) -> str:
16
+ if value.endswith(suffix):
17
+ return value[: -len(suffix)]
18
+ return value
19
+
20
+
21
+ def _base_site_name_from_enb_name(enb_name: object) -> str:
22
+ if not isinstance(enb_name, str):
23
+ return ""
24
+ s = enb_name.strip()
25
+ s = _strip_suffix(s, "_4G")
26
+ s = _strip_suffix(s, "_4g")
27
+ return s
28
+
29
+
30
+ def _band_from_cell_name(cell_name: object) -> str:
31
+ if not isinstance(cell_name, str):
32
+ return ""
33
+ s = cell_name.upper()
34
+ for band in ["L800", "L1800", "L2600", "L2300", "L700"]:
35
+ if f"_{band}" in s or s.endswith(band):
36
+ return band
37
+ return ""
38
+
39
+
40
+ def _sector_from_cell_name(cell_name: object) -> Optional[int]:
41
+ if not isinstance(cell_name, str):
42
+ return None
43
+ m = re.search(r"_(\d+)_L\d+\b", cell_name.upper())
44
+ if not m:
45
+ return None
46
+ try:
47
+ return int(m.group(1))
48
+ except ValueError:
49
+ return None
50
+
51
+
52
+ def _dl_mimo_mode_for_band(band: str) -> str:
53
+ if band in {"L2600"}:
54
+ return "4x4"
55
+ return "2x2"
56
+
57
+
58
+ def _ul_earfcn_from_dl(dl_earfcn: int) -> int:
59
+ return int(dl_earfcn) + 18000
60
+
61
+
62
+ def _build_output_columns(band_blocks: list[str]) -> list[str]:
63
+ cols = [
64
+ "Unique Site ID",
65
+ "Config",
66
+ "Site Name",
67
+ "$mrbtsid",
68
+ "$lnbtsid",
69
+ "$enbname",
70
+ "$mcc",
71
+ "$mnc",
72
+ ]
73
+
74
+ cell_idx = 1
75
+ for band_block_idx, _ in enumerate(band_blocks, start=1):
76
+ for _slot in range(4):
77
+ cols.extend(
78
+ [
79
+ f"$lncelname{cell_idx}",
80
+ f"$Eutracelid{cell_idx}",
81
+ f"$pci{cell_idx}",
82
+ f"$rsi{cell_idx}",
83
+ f"$ltemaxpower{cell_idx}",
84
+ ]
85
+ )
86
+ cell_idx += 1
87
+
88
+ cols.extend(
89
+ [
90
+ f"$tac{band_block_idx}",
91
+ f"$dlMimoMode{band_block_idx}",
92
+ f"$ChBw{band_block_idx}",
93
+ f"$dlearfcnlte{band_block_idx}",
94
+ f"$ulearfcnlte{band_block_idx}",
95
+ ]
96
+ )
97
+
98
+ return cols
99
+
100
+
101
+ def read_ciq_4g_brut(ciq_file) -> pd.DataFrame:
102
+ if hasattr(ciq_file, "seek"):
103
+ ciq_file.seek(0)
104
+
105
+ df = pd.read_excel(ciq_file, engine="calamine")
106
+ df.columns = df.columns.astype(str).str.strip()
107
+
108
+ required = [
109
+ "eNodeBName",
110
+ "CellName",
111
+ "DlEarfcn",
112
+ "eNodeB Id",
113
+ "Local Cell ID",
114
+ "TAC",
115
+ "Physical cell ID",
116
+ "Root sequence index",
117
+ ]
118
+ missing = [c for c in required if c not in df.columns]
119
+ if missing:
120
+ raise ValueError(f"CIQ 4G brut is missing required columns: {missing}")
121
+
122
+ return df
123
+
124
+
125
+ def generate_ciq_4g_sheet(
126
+ ciq_df: pd.DataFrame,
127
+ year_suffix: str,
128
+ bands: str,
129
+ mcc: int,
130
+ mnc: int,
131
+ band_blocks: Optional[list[str]] = None,
132
+ ch_bw: int = 20,
133
+ lte_max_power: int = 460,
134
+ ) -> pd.DataFrame:
135
+ if band_blocks is None:
136
+ band_blocks = ["L800", "L1800", "L2600"]
137
+
138
+ output_cols = _build_output_columns(band_blocks)
139
+
140
+ rows_out: list[list[object]] = []
141
+
142
+ for enb_id_raw, site_rows in ciq_df.groupby("eNodeB Id", dropna=False):
143
+ enb_id = _parse_int(enb_id_raw)
144
+ if enb_id is None:
145
+ continue
146
+
147
+ enb_name = str(site_rows["eNodeBName"].dropna().iloc[0]).strip()
148
+ base_site = _base_site_name_from_enb_name(enb_name)
149
+
150
+ site_name = f"{base_site}_{year_suffix}_{bands}_NA"
151
+ enbname_out = f"{enb_name}_NA"
152
+
153
+ row_map: dict[str, object] = {
154
+ "Unique Site ID": enb_id,
155
+ "Config": bands,
156
+ "Site Name": site_name,
157
+ "$mrbtsid": int(10000 + enb_id),
158
+ "$lnbtsid": enb_id,
159
+ "$enbname": enbname_out,
160
+ "$mcc": int(mcc),
161
+ "$mnc": str(int(mnc)).zfill(2),
162
+ }
163
+
164
+ cell_slot_idx = 1
165
+ for block_idx, band in enumerate(band_blocks, start=1):
166
+ sub = site_rows[
167
+ site_rows["CellName"].apply(_band_from_cell_name) == band
168
+ ].copy()
169
+
170
+ sub["_sector"] = sub["CellName"].apply(_sector_from_cell_name)
171
+ sub = sub.sort_values(by=["_sector", "Local Cell ID"], na_position="last")
172
+
173
+ for slot in range(4):
174
+ if slot < len(sub):
175
+ r = sub.iloc[slot]
176
+ cell_name = str(r.get("CellName")).strip()
177
+ dl_earfcn = _parse_int(r.get("DlEarfcn"))
178
+ local_cell_id = _parse_int(r.get("Local Cell ID"))
179
+ pci = _parse_int(r.get("Physical cell ID"))
180
+ rsi = _parse_int(r.get("Root sequence index"))
181
+
182
+ row_map[f"$lncelname{cell_slot_idx}"] = f"{cell_name}_NA"
183
+ row_map[f"$Eutracelid{cell_slot_idx}"] = local_cell_id
184
+ row_map[f"$pci{cell_slot_idx}"] = pci
185
+ row_map[f"$rsi{cell_slot_idx}"] = rsi
186
+ row_map[f"$ltemaxpower{cell_slot_idx}"] = int(lte_max_power)
187
+
188
+ cell_slot_idx += 1
189
+
190
+ if not sub.empty:
191
+ tac = _parse_int(sub.iloc[0].get("TAC"))
192
+ dl_earfcn = _parse_int(sub.iloc[0].get("DlEarfcn"))
193
+
194
+ row_map[f"$tac{block_idx}"] = tac
195
+ row_map[f"$dlMimoMode{block_idx}"] = _dl_mimo_mode_for_band(band)
196
+ row_map[f"$ChBw{block_idx}"] = int(ch_bw)
197
+ row_map[f"$dlearfcnlte{block_idx}"] = dl_earfcn
198
+ row_map[f"$ulearfcnlte{block_idx}"] = (
199
+ _ul_earfcn_from_dl(dl_earfcn) if dl_earfcn is not None else None
200
+ )
201
+
202
+ rows_out.append([row_map.get(c) for c in output_cols])
203
+
204
+ return pd.DataFrame(rows_out, columns=output_cols)
205
+
206
+
207
+ def generate_ciq_4g_excel(
208
+ ciq_file,
209
+ year_suffix: str = "25",
210
+ bands: str = "G9G18U9U21L8L18L26",
211
+ mcc: int = 610,
212
+ mnc: int = 2,
213
+ ) -> tuple[dict[str, pd.DataFrame], bytes]:
214
+ df_in = read_ciq_4g_brut(ciq_file)
215
+ df_out = generate_ciq_4g_sheet(
216
+ df_in,
217
+ year_suffix=year_suffix,
218
+ bands=bands,
219
+ mcc=mcc,
220
+ mnc=mnc,
221
+ )
222
+
223
+ sheets: dict[str, pd.DataFrame] = {"CIQ_4G": df_out}
224
+
225
+ bytes_io = io.BytesIO()
226
+ with pd.ExcelWriter(bytes_io, engine="xlsxwriter") as writer:
227
+ for sheet_name, df in sheets.items():
228
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
229
+
230
+ return sheets, bytes_io.getvalue()