DavMelchi commited on
Commit
b9e6156
·
1 Parent(s): dee9e18

Add CIQ 2G Generator with BCF assignment logic, site parsing from CIQ brut Excel, dump BTS column extraction, band/sector detection, configuration building, and multi-sheet Excel export with placeholder sheets for BTS, GPRS, AMR, HOC, POC, MAL, PLMNPERMITTED, and TRX

Browse files
Files changed (3) hide show
  1. app.py +1 -0
  2. apps/ciq_2g_generator.py +48 -0
  3. queries/process_ciq_2g.py +362 -0
app.py CHANGED
@@ -118,6 +118,7 @@ if check_password():
118
  st.Page(
119
  "apps/parameters_distribution.py", title="📊Parameters distribution"
120
  ),
 
121
  st.Page("apps/core_dump_page.py", title="📠Parse dump core"),
122
  st.Page("apps/gps_converter.py", title="🧭GPS Converter"),
123
  st.Page("apps/distance.py", title="🛰Distance Calculator"),
 
118
  st.Page(
119
  "apps/parameters_distribution.py", title="📊Parameters distribution"
120
  ),
121
+ st.Page("apps/ciq_2g_generator.py", title="🧾 CIQ 2G Generator"),
122
  st.Page("apps/core_dump_page.py", title="📠Parse dump core"),
123
  st.Page("apps/gps_converter.py", title="🧭GPS Converter"),
124
  st.Page("apps/distance.py", title="🛰Distance Calculator"),
apps/ciq_2g_generator.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import streamlit as st
3
+
4
+ from queries.process_ciq_2g import generate_ciq_2g_excel
5
+
6
+ st.title("CIQ 2G Generator")
7
+
8
+ col1, col2 = st.columns(2)
9
+ with col1:
10
+ dump_file = st.file_uploader("Upload dump file", type=["xlsb"], key="ciq2g_dump")
11
+ with col2:
12
+ ciq_file = st.file_uploader(
13
+ "Upload CIQ brut 2G (Excel)", type=["xlsx", "xls"], key="ciq2g_ciq"
14
+ )
15
+
16
+ if dump_file is None or ciq_file is None:
17
+ st.info("Upload dump xlsb + CIQ brut Excel to generate CIQ 2G.")
18
+ st.stop()
19
+
20
+ if st.button("Generate", type="primary"):
21
+ try:
22
+ with st.spinner("Generating CIQ 2G... (dump is heavy)"):
23
+ sheets, excel_bytes = generate_ciq_2g_excel(dump_file, ciq_file)
24
+ st.session_state["ciq2g_sheets"] = sheets
25
+ st.session_state["ciq2g_excel_bytes"] = excel_bytes
26
+ st.success("CIQ 2G generated")
27
+ except Exception as e:
28
+ st.error(f"Error: {e}")
29
+
30
+ sheets = st.session_state.get("ciq2g_sheets")
31
+ excel_bytes = st.session_state.get("ciq2g_excel_bytes")
32
+
33
+ if sheets:
34
+ tab_names = list(sheets.keys())
35
+ tabs = st.tabs(tab_names)
36
+ for t, name in zip(tabs, tab_names):
37
+ with t:
38
+ df: pd.DataFrame = sheets[name]
39
+ st.dataframe(df, use_container_width=True)
40
+
41
+ if excel_bytes:
42
+ st.download_button(
43
+ label="Download CIQ 2G Excel",
44
+ data=excel_bytes,
45
+ file_name="CIQ_2G.xlsx",
46
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
47
+ type="primary",
48
+ )
queries/process_ciq_2g.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import re
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ import pandas as pd
7
+
8
+ REQUIRED_DUMP_BTS_COLS = ["BSC", "BCF", "BTS", "usedMobileAllocation"]
9
+
10
+
11
+ def _normalize_col(col: object) -> str:
12
+ return re.sub(r"[^0-9A-Za-z]", "", str(col))
13
+
14
+
15
+ def _clean_columns(df: pd.DataFrame) -> pd.DataFrame:
16
+ df = df.copy()
17
+ df.columns = [_normalize_col(c) for c in df.columns]
18
+ return df
19
+
20
+
21
+ def _read_dump_bts_required_columns(dump_file) -> pd.DataFrame:
22
+ if hasattr(dump_file, "seek"):
23
+ dump_file.seek(0)
24
+
25
+ hdr = pd.read_excel(
26
+ dump_file,
27
+ sheet_name="BTS",
28
+ engine="calamine",
29
+ skiprows=[0],
30
+ nrows=0,
31
+ )
32
+
33
+ original_cols = list(hdr.columns)
34
+ normalized_to_original: dict[str, str] = {}
35
+ for c in original_cols:
36
+ n = _normalize_col(c)
37
+ if n and n not in normalized_to_original:
38
+ normalized_to_original[n] = c
39
+
40
+ missing = [c for c in REQUIRED_DUMP_BTS_COLS if c not in normalized_to_original]
41
+ if missing:
42
+ raise ValueError(
43
+ f"Dump sheet 'BTS' is missing required columns after cleanup: {missing}. "
44
+ f"Found columns (normalized): {sorted(normalized_to_original.keys())[:50]}"
45
+ )
46
+
47
+ usecols = [normalized_to_original[c] for c in REQUIRED_DUMP_BTS_COLS]
48
+
49
+ if hasattr(dump_file, "seek"):
50
+ dump_file.seek(0)
51
+
52
+ df = pd.read_excel(
53
+ dump_file,
54
+ sheet_name="BTS",
55
+ engine="calamine",
56
+ skiprows=[0],
57
+ usecols=usecols,
58
+ )
59
+ df = _clean_columns(df)
60
+
61
+ df = df[REQUIRED_DUMP_BTS_COLS]
62
+ for c in ["BSC", "BCF", "BTS", "usedMobileAllocation"]:
63
+ df[c] = pd.to_numeric(df[c], errors="coerce")
64
+
65
+ return df
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class _PlannedSite:
70
+ site_name: str
71
+ site_number: int
72
+ bsc: int
73
+ bsc_name: str
74
+ name: str
75
+ configuration: str
76
+ assigned_bcf: Optional[int]
77
+ needed_bts_ids: tuple[int, ...]
78
+
79
+
80
+ def _parse_site_number(site: object) -> int:
81
+ if not isinstance(site, str):
82
+ return 0
83
+ m = re.match(r"^(\d+)", site.strip())
84
+ return int(m.group(1)) if m else 0
85
+
86
+
87
+ def _extract_band_and_sector(cell_name: object) -> tuple[Optional[str], Optional[int]]:
88
+ if not isinstance(cell_name, str):
89
+ return None, None
90
+
91
+ parts = cell_name.strip().split("_")
92
+ for i in range(len(parts) - 1):
93
+ if parts[i].isdigit() and parts[i + 1] in {"900", "1800"}:
94
+ sector = int(parts[i])
95
+ band = "G9" if parts[i + 1] == "900" else "G18"
96
+ return band, sector
97
+
98
+ if cell_name.endswith("_900"):
99
+ return "G9", None
100
+ if cell_name.endswith("_1800"):
101
+ return "G18", None
102
+
103
+ return None, None
104
+
105
+
106
+ def _build_configuration(site_rows: pd.DataFrame) -> str:
107
+ rows = site_rows.copy()
108
+
109
+ rows["sector"] = pd.to_numeric(rows.get("sector"), errors="coerce")
110
+ rows["Nbre_TRE_DR"] = pd.to_numeric(rows.get("Nbre_TRE_DR"), errors="coerce")
111
+
112
+ configs: list[str] = []
113
+ for band in ["G9", "G18"]:
114
+ sub = rows[rows["band"] == band]
115
+ if sub.empty:
116
+ continue
117
+
118
+ sub = (
119
+ sub.dropna(subset=["Nbre_TRE_DR"])
120
+ .drop_duplicates(subset=["sector"], keep="first")
121
+ .sort_values(by=["sector"], na_position="last")
122
+ )
123
+ digits = "".join(str(int(v)) for v in sub["Nbre_TRE_DR"].tolist())
124
+ if digits:
125
+ configs.append(f"{band}-{digits}")
126
+
127
+ return ", ".join(configs)
128
+
129
+
130
+ def _needed_bts_ids_from_site_rows(
131
+ bcf: int, site_rows: pd.DataFrame
132
+ ) -> tuple[int, ...]:
133
+ ids: set[int] = set()
134
+
135
+ offset_map = {
136
+ ("G9", 1): 1,
137
+ ("G9", 2): 2,
138
+ ("G9", 3): 3,
139
+ ("G18", 1): 4,
140
+ ("G18", 2): 5,
141
+ ("G18", 3): 6,
142
+ }
143
+
144
+ for _, r in site_rows.iterrows():
145
+ band = r.get("band")
146
+ sector = r.get("sector")
147
+ if (
148
+ band in {"G9", "G18"}
149
+ and isinstance(sector, (int, float))
150
+ and not pd.isna(sector)
151
+ ):
152
+ sector_int = int(sector)
153
+ off = offset_map.get((band, sector_int))
154
+ if off is not None:
155
+ ids.add(bcf + off)
156
+
157
+ return tuple(sorted(ids))
158
+
159
+
160
+ def _parse_ciq_sites(ciq_file) -> list[_PlannedSite]:
161
+ if hasattr(ciq_file, "seek"):
162
+ ciq_file.seek(0)
163
+
164
+ df = pd.read_excel(ciq_file, engine="calamine")
165
+
166
+ df.columns = df.columns.astype(str).str.strip()
167
+
168
+ required = ["Sites", "NOM_CELLULE", "Nbre_TRE_DR", "Nom BSC", "BSC ID"]
169
+ missing = [c for c in required if c not in df.columns]
170
+ if missing:
171
+ raise ValueError(f"CIQ brut is missing required columns: {missing}")
172
+
173
+ df = df[required].copy()
174
+
175
+ df["site_number"] = df["Sites"].apply(_parse_site_number)
176
+ df["BSC ID"] = pd.to_numeric(df["BSC ID"], errors="coerce")
177
+ df["Nbre_TRE_DR"] = pd.to_numeric(df["Nbre_TRE_DR"], errors="coerce")
178
+
179
+ bands_sectors = df["NOM_CELLULE"].apply(_extract_band_and_sector)
180
+ df["band"] = bands_sectors.apply(lambda x: x[0])
181
+ df["sector"] = bands_sectors.apply(lambda x: x[1])
182
+
183
+ sites: list[_PlannedSite] = []
184
+
185
+ for site_name, site_rows in df.groupby("Sites", dropna=False):
186
+ if not isinstance(site_name, str) or not site_name.strip():
187
+ continue
188
+
189
+ bsc_series = site_rows["BSC ID"].dropna()
190
+ if bsc_series.empty:
191
+ raise ValueError(f"Missing BSC ID for site '{site_name}'")
192
+ bsc = int(bsc_series.iloc[0])
193
+
194
+ bsc_name_series = site_rows["Nom BSC"].dropna()
195
+ bsc_name = str(bsc_name_series.iloc[0]) if not bsc_name_series.empty else ""
196
+
197
+ site_number = int(site_rows["site_number"].dropna().iloc[0])
198
+
199
+ configuration = _build_configuration(site_rows)
200
+
201
+ sites.append(
202
+ _PlannedSite(
203
+ site_name=site_name,
204
+ site_number=site_number,
205
+ bsc=bsc,
206
+ bsc_name=bsc_name,
207
+ name=f"{site_name}_NA",
208
+ configuration=configuration,
209
+ assigned_bcf=None,
210
+ needed_bts_ids=(),
211
+ )
212
+ )
213
+
214
+ return sorted(sites, key=lambda s: (s.bsc, s.site_number, s.site_name))
215
+
216
+
217
+ def _assign_bcfs(
218
+ dump_bts: pd.DataFrame, planned_sites: list[_PlannedSite], ciq_file
219
+ ) -> list[_PlannedSite]:
220
+ if hasattr(ciq_file, "seek"):
221
+ ciq_file.seek(0)
222
+
223
+ ciq_df = pd.read_excel(ciq_file, engine="calamine")
224
+ ciq_df.columns = ciq_df.columns.astype(str).str.strip()
225
+ ciq_df = ciq_df[["Sites", "NOM_CELLULE", "Nbre_TRE_DR", "Nom BSC", "BSC ID"]].copy()
226
+
227
+ ciq_df["BSC ID"] = pd.to_numeric(ciq_df["BSC ID"], errors="coerce")
228
+ ciq_df["Nbre_TRE_DR"] = pd.to_numeric(ciq_df["Nbre_TRE_DR"], errors="coerce")
229
+
230
+ bands_sectors = ciq_df["NOM_CELLULE"].apply(_extract_band_and_sector)
231
+ ciq_df["band"] = bands_sectors.apply(lambda x: x[0])
232
+ ciq_df["sector"] = bands_sectors.apply(lambda x: x[1])
233
+
234
+ dump_bts = dump_bts.dropna(subset=["BSC"])
235
+
236
+ assigned: list[_PlannedSite] = []
237
+
238
+ sites_by_bsc: dict[int, list[_PlannedSite]] = {}
239
+ for s in planned_sites:
240
+ sites_by_bsc.setdefault(s.bsc, []).append(s)
241
+
242
+ for bsc, sites_in_bsc in sites_by_bsc.items():
243
+ sub_dump = dump_bts[dump_bts["BSC"].fillna(-1).astype(int) == int(bsc)]
244
+
245
+ used_bcfs: set[int] = set(
246
+ pd.to_numeric(sub_dump["BCF"], errors="coerce")
247
+ .dropna()
248
+ .astype(int)
249
+ .tolist()
250
+ )
251
+ used_bts: set[int] = set(
252
+ pd.to_numeric(sub_dump["BTS"], errors="coerce")
253
+ .dropna()
254
+ .astype(int)
255
+ .tolist()
256
+ )
257
+ used_mal: set[int] = set(
258
+ pd.to_numeric(sub_dump["usedMobileAllocation"], errors="coerce")
259
+ .dropna()
260
+ .astype(int)
261
+ .tolist()
262
+ )
263
+
264
+ sites_in_bsc_sorted = sorted(
265
+ sites_in_bsc, key=lambda s: (s.site_number, s.site_name)
266
+ )
267
+
268
+ for site in sites_in_bsc_sorted:
269
+ site_rows = ciq_df[ciq_df["Sites"] == site.site_name]
270
+ if site_rows.empty:
271
+ raise ValueError(f"No CIQ rows found for site '{site.site_name}'")
272
+
273
+ assigned_bcf = None
274
+ assigned_needed_ids: Optional[tuple[int, ...]] = None
275
+
276
+ for cand in range(10, 4401, 10):
277
+ if cand in used_bcfs:
278
+ continue
279
+
280
+ site_needed_ids = _needed_bts_ids_from_site_rows(cand, site_rows)
281
+ if not site_needed_ids:
282
+ continue
283
+
284
+ required_ids = tuple(cand + i for i in range(1, 7))
285
+
286
+ if any((i in used_bts) or (i in used_mal) for i in required_ids):
287
+ continue
288
+
289
+ assigned_bcf = cand
290
+ assigned_needed_ids = site_needed_ids
291
+ break
292
+
293
+ if assigned_bcf is None or assigned_needed_ids is None:
294
+ raise ValueError(
295
+ f"No available BCF found for site '{site.site_name}' on BSC {bsc}"
296
+ )
297
+
298
+ used_bcfs.add(assigned_bcf)
299
+ reserved_ids = [assigned_bcf + i for i in range(1, 7)]
300
+ used_bts.update(reserved_ids)
301
+ used_mal.update(reserved_ids)
302
+
303
+ assigned.append(
304
+ _PlannedSite(
305
+ site_name=site.site_name,
306
+ site_number=site.site_number,
307
+ bsc=site.bsc,
308
+ bsc_name=site.bsc_name,
309
+ name=site.name,
310
+ configuration=site.configuration,
311
+ assigned_bcf=int(assigned_bcf),
312
+ needed_bts_ids=assigned_needed_ids,
313
+ )
314
+ )
315
+
316
+ return sorted(assigned, key=lambda s: (s.bsc, s.site_number, s.site_name))
317
+
318
+
319
+ def build_bcf_sheet(dump_file, ciq_file) -> pd.DataFrame:
320
+ dump_bts = _read_dump_bts_required_columns(dump_file)
321
+ planned_sites = _parse_ciq_sites(ciq_file)
322
+ assigned_sites = _assign_bcfs(dump_bts, planned_sites, ciq_file)
323
+
324
+ rows = []
325
+ for i, s in enumerate(assigned_sites, start=1):
326
+ rows.append(
327
+ {
328
+ "S. No.": i,
329
+ "Site Number": s.site_number,
330
+ "BSC": s.bsc,
331
+ "BSC Name": s.bsc_name,
332
+ "BCF": s.assigned_bcf,
333
+ "name": s.name,
334
+ "Configuration": s.configuration,
335
+ }
336
+ )
337
+
338
+ df_bcf = pd.DataFrame(rows)
339
+ return df_bcf
340
+
341
+
342
+ def generate_ciq_2g_excel(dump_file, ciq_file) -> tuple[dict[str, pd.DataFrame], bytes]:
343
+ df_bcf = build_bcf_sheet(dump_file, ciq_file)
344
+
345
+ sheets: dict[str, pd.DataFrame] = {
346
+ "BCF": df_bcf,
347
+ "BTS": pd.DataFrame(),
348
+ "BTS_GPRS": pd.DataFrame(),
349
+ "BTS_AMR": pd.DataFrame(),
350
+ "HOC": pd.DataFrame(),
351
+ "POC": pd.DataFrame(),
352
+ "MAL": pd.DataFrame(),
353
+ "BTS_PLMNPERMITTED": pd.DataFrame(),
354
+ "TRX": pd.DataFrame(),
355
+ }
356
+
357
+ bytes_io = io.BytesIO()
358
+ with pd.ExcelWriter(bytes_io, engine="xlsxwriter") as writer:
359
+ for sheet_name, df in sheets.items():
360
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
361
+
362
+ return sheets, bytes_io.getvalue()