eas-3d-pattern 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/PKG-INFO +4 -1
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/pyproject.toml +4 -1
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/__init__.py +9 -2
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/parser.py +20 -0
- eas_3d_pattern-0.1.2/src/eas_3d_pattern/util_func/__init__.py +0 -0
- eas_3d_pattern-0.1.2/src/eas_3d_pattern/util_func/report.py +237 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/LICENSE +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/README.md +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL1_2100_A-Y1_RFp01_T00p0_P45_NonUniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL2_2100_A-Y1_RFp01_T00p0_P45_UniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL3_3700_EID12345_T00p0_P45_NonUniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL4_3700_BID67890_T00p0_P45_UniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/sample_data/__init__.py +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/schema_manager.py +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/schemas/NGMN_BASTA_AA_3drp_JSON_Schema_WP3_0_latest.json +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/schemas/__init__.py +0 -0
- {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/sector_definitions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: eas-3d-pattern
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A tool to visualize 3D antenna pattern files in JSON format.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -38,10 +38,13 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
38
38
|
Classifier: Operating System :: OS Independent
|
|
39
39
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
40
40
|
Requires-Dist: jsonschema (>=4.24.0,<5.0.0)
|
|
41
|
+
Requires-Dist: kaleido (>=0.1.0,<0.2.0)
|
|
41
42
|
Requires-Dist: numpy (>=2.0.0,<3.0.0)
|
|
43
|
+
Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
|
|
42
44
|
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
43
45
|
Requires-Dist: plotly (>=5.24.1,<6.0.0)
|
|
44
46
|
Requires-Dist: requests (>=2.32.2,<3.0.0)
|
|
47
|
+
Requires-Dist: tqdm (>=4.67.1,<5.0.0)
|
|
45
48
|
Requires-Dist: xarray (>=2025.3.1,<2026.0.0)
|
|
46
49
|
Project-URL: Homepage, https://www.ericsson.com/antenna-system
|
|
47
50
|
Project-URL: Repository, https://github.com/Ericsson/eas-3d-pattern
|
|
@@ -13,10 +13,13 @@ keywords = ["antenna", "pattern", "3d pattern", "visualization", "NGMN", "BASTA"
|
|
|
13
13
|
dependencies = [
|
|
14
14
|
"numpy (>=2.0.0,<3.0.0)",
|
|
15
15
|
"pandas (>=2.2.2,<3.0.0)",
|
|
16
|
+
"openpyxl (>=3.1.5, <4.0.0)",
|
|
16
17
|
"jsonschema (>=4.24.0,<5.0.0)",
|
|
17
18
|
"xarray (>=2025.3.1,<2026.0.0)",
|
|
18
19
|
"plotly (>=5.24.1,<6.0.0)",
|
|
20
|
+
"kaleido (>=0.1.0, <0.2.0)",
|
|
19
21
|
"requests (>=2.32.2,<3.0.0)",
|
|
22
|
+
"tqdm (>=4.67.1,<5.0.0)",
|
|
20
23
|
]
|
|
21
24
|
|
|
22
25
|
classifiers = [
|
|
@@ -31,7 +34,7 @@ classifiers = [
|
|
|
31
34
|
"Operating System :: OS Independent",
|
|
32
35
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
33
36
|
]
|
|
34
|
-
version = "0.1.
|
|
37
|
+
version = "0.1.2"
|
|
35
38
|
|
|
36
39
|
[project.urls]
|
|
37
40
|
homepage = "https://www.ericsson.com/antenna-system"
|
|
@@ -4,12 +4,19 @@ from .parser import AntennaPattern
|
|
|
4
4
|
from .sample_data import SAMPLE_JSON
|
|
5
5
|
from .schema_manager import NGMNSchema
|
|
6
6
|
from .sector_definitions import SectorDefinition
|
|
7
|
+
from .util_func.report import generate_report_eas
|
|
7
8
|
|
|
8
9
|
LIBRARY_PACKAGE_NAME = __name__
|
|
9
10
|
library_root_logger = logging.getLogger(LIBRARY_PACKAGE_NAME)
|
|
10
11
|
if not library_root_logger.hasHandlers():
|
|
11
12
|
library_root_logger.addHandler(logging.NullHandler())
|
|
12
13
|
|
|
13
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SAMPLE_JSON",
|
|
16
|
+
"AntennaPattern",
|
|
17
|
+
"NGMNSchema",
|
|
18
|
+
"SectorDefinition",
|
|
19
|
+
"generate_report_eas",
|
|
20
|
+
]
|
|
14
21
|
|
|
15
|
-
__version__ = "0.1.
|
|
22
|
+
__version__ = "0.1.1"
|
|
@@ -485,6 +485,26 @@ class AntennaPattern:
|
|
|
485
485
|
)
|
|
486
486
|
return Pattern_3D.sortby(["Theta", "Phi"])
|
|
487
487
|
|
|
488
|
+
def get_metadata_dict(self) -> dict[str, Any]:
|
|
489
|
+
"""Get meta data dictionary of the antenna pattern data.
|
|
490
|
+
|
|
491
|
+
Metadata is everything beside 'Data_Set' and 'Data_Set_Row_Structure' from the JSON file.
|
|
492
|
+
The metadata can be used to enrich pandas dataframes.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
None
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
dict: A dictionary with key and value pairs from the JSON file.
|
|
499
|
+
|
|
500
|
+
Example:
|
|
501
|
+
>>> meta_dict = antenna_pattern.get_metadata_dict()
|
|
502
|
+
"""
|
|
503
|
+
metadata = self.raw_data.copy()
|
|
504
|
+
metadata.pop("Data_Set", None)
|
|
505
|
+
metadata.pop("Data_Set_Row_Structure", None)
|
|
506
|
+
return metadata
|
|
507
|
+
|
|
488
508
|
def calculate_beam_efficiency(
|
|
489
509
|
self, sector_definitions: SectorDefinition | None = None, powersum: bool = True
|
|
490
510
|
) -> dict[str, float]:
|
|
File without changes
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
|
|
9
|
+
from eas_3d_pattern import AntennaPattern, SectorDefinition
|
|
10
|
+
|
|
11
|
+
SUBBANDS_DEFAULT = {
|
|
12
|
+
"698-806": (698, 806),
|
|
13
|
+
"791-862": (791, 862),
|
|
14
|
+
"824-894": (824, 894),
|
|
15
|
+
"880-960": (880, 960),
|
|
16
|
+
"1427-1518": (1427, 1518),
|
|
17
|
+
"1695-1880": (1695, 1880),
|
|
18
|
+
"1850-1990": (1850, 1990),
|
|
19
|
+
"1920-2200": (1920, 2200),
|
|
20
|
+
"2200-2490": (2200, 2490),
|
|
21
|
+
"2490-2690": (2490, 2690),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def temporarily_set_loglevel(logger_name: str, level: int) -> Iterator[None]:
|
|
27
|
+
"""A context manager to temporarily set the log level for to supress most messages during file reading."""
|
|
28
|
+
logger = logging.getLogger(logger_name)
|
|
29
|
+
old_level = logger.level
|
|
30
|
+
try:
|
|
31
|
+
logger.setLevel(level)
|
|
32
|
+
yield
|
|
33
|
+
finally:
|
|
34
|
+
logger.setLevel(old_level)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def generate_report_eas(
|
|
38
|
+
input_directory: Path | str,
|
|
39
|
+
output_directory: Path | str,
|
|
40
|
+
plot: bool = False,
|
|
41
|
+
subbands: dict[str, tuple[int, int]] = SUBBANDS_DEFAULT,
|
|
42
|
+
) -> pd.DataFrame:
|
|
43
|
+
"""Generates a excel report for EAS JSON files.
|
|
44
|
+
|
|
45
|
+
The report contains all antenna data, beam efficiency and sector information in the JSON files within a directory.
|
|
46
|
+
Additionally, aggregated data per array and per tilt and per subband is also generated.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
input_directory (Path | str): The path to the directory containing the JSON files.
|
|
50
|
+
output_directory (Path | str): The path to the directory where the report will be saved.
|
|
51
|
+
plot (bool, optional): Whether to generate plots of the antenna patterns. Default is False.
|
|
52
|
+
subbands (dict[str, tuple[int, int]], optional): A dictionary of subbands and their corresponding frequency ranges. Default is SUBBANDS_DEFAULT.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
pd.DataFrame: A pandas DataFrame containing the raw data for the report.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> from eas_3d_pattern import generate_report_eas
|
|
59
|
+
>>> df = generate_report_eas("path/to/directory", "path/to/output_directory")
|
|
60
|
+
|
|
61
|
+
>>> df = generate_report_eas(
|
|
62
|
+
... "path/to/directory", "path/to/output_directory", plot=True
|
|
63
|
+
... ) # with png plotting
|
|
64
|
+
|
|
65
|
+
>>> my_own_subbands = {
|
|
66
|
+
"Carrier1": (730, 750),
|
|
67
|
+
"Carrier2": (1970, 1990),
|
|
68
|
+
}
|
|
69
|
+
>>> df = generate_report_eas(
|
|
70
|
+
... "path/to/directory", "path/to/output_directory", subband=my_own_subbands
|
|
71
|
+
... ) # with own subband
|
|
72
|
+
"""
|
|
73
|
+
input_directory = Path(input_directory)
|
|
74
|
+
files = list(input_directory.glob("*.json"))
|
|
75
|
+
if len(files) == 0:
|
|
76
|
+
raise ValueError("Report: No .json files found in the directory.")
|
|
77
|
+
output_directory = Path(output_directory)
|
|
78
|
+
output_directory.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
|
|
80
|
+
df_list: list[pd.DataFrame] = []
|
|
81
|
+
with temporarily_set_loglevel(logger_name="eas_3d_pattern", level=logging.ERROR):
|
|
82
|
+
for file in tqdm(files):
|
|
83
|
+
data = _process_a_file(file)
|
|
84
|
+
if data is not None:
|
|
85
|
+
data_row, pattern, sectors = data
|
|
86
|
+
df_list.append(data_row)
|
|
87
|
+
if plot:
|
|
88
|
+
_save_figure(pattern, sectors, output_directory)
|
|
89
|
+
df_raw = pd.concat(df_list, ignore_index=True)
|
|
90
|
+
|
|
91
|
+
report_name = output_directory / "BEreport.xlsx"
|
|
92
|
+
_generate_excel_report(df_raw, report_name, subbands)
|
|
93
|
+
return df_raw
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _process_a_file(
|
|
97
|
+
file_path: Path,
|
|
98
|
+
) -> tuple[pd.DataFrame, AntennaPattern, SectorDefinition] | None:
|
|
99
|
+
"""Process a file and return the processed data.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
file_path (Path): The path to the file to be processed.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
tuple[pd.DataFrame, AntennaPattern, SectorDefinition] | None:
|
|
106
|
+
A tuple containing the processed data, the AntennaPattern instance,
|
|
107
|
+
and the SectorDefinition instance. If an exception is raised, None is
|
|
108
|
+
returned.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
Exception: If an error occurs during the processing of the file.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
pattern = AntennaPattern(str(file_path), validate=False)
|
|
115
|
+
data = pattern.get_metadata_dict()
|
|
116
|
+
top_border = pattern.calculate_top_3db_point(power=False)
|
|
117
|
+
eas_sectors = SectorDefinition(load_default=True, top_border=top_border)
|
|
118
|
+
eff = pattern.calculate_beam_efficiency(sector_definitions=eas_sectors)
|
|
119
|
+
data.update({f"{k}": v * 100 for k, v in eff.items()})
|
|
120
|
+
data.update(
|
|
121
|
+
{
|
|
122
|
+
f"{k}_limits": [
|
|
123
|
+
v.phi_min[0],
|
|
124
|
+
v.theta_min[0],
|
|
125
|
+
v.phi_max[0],
|
|
126
|
+
v.theta_max[0],
|
|
127
|
+
]
|
|
128
|
+
for k, v in eas_sectors.sectors.items()
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
data["filepath"] = str(pattern.data_filepath)
|
|
132
|
+
return (pd.json_normalize(data, sep="_"), pattern, eas_sectors)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logging.error(
|
|
135
|
+
f"Reporting: Skipping corrupted or invalid file '{file_path.name}': {e}"
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _save_figure(
|
|
141
|
+
pattern: AntennaPattern,
|
|
142
|
+
sector_definitions: SectorDefinition,
|
|
143
|
+
output_directory: Path,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Saves the plot of the given AntennaPattern and SectorDefinition to a PNG file.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
pattern (AntennaPattern): The AntennaPattern to plot.
|
|
149
|
+
sector_definitions (SectorDefinition): The SectorDefinition to use for plotting.
|
|
150
|
+
output_directory (Path): The directory to save the plot to.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
None
|
|
154
|
+
"""
|
|
155
|
+
fig = pattern.plot(show_fig=False)
|
|
156
|
+
if fig is None:
|
|
157
|
+
return
|
|
158
|
+
for k in sector_definitions.sectors:
|
|
159
|
+
fig.add_shape(
|
|
160
|
+
type="rect",
|
|
161
|
+
x0=sector_definitions.sectors[k].phi_min[0],
|
|
162
|
+
y0=sector_definitions.sectors[k].theta_min[0],
|
|
163
|
+
x1=sector_definitions.sectors[k].phi_max[0],
|
|
164
|
+
y1=sector_definitions.sectors[k].theta_max[0],
|
|
165
|
+
line={"color": "White"},
|
|
166
|
+
)
|
|
167
|
+
fig.write_image(
|
|
168
|
+
output_directory / f"{Path(pattern.data_filepath).stem}.png", format="png"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _generate_excel_report(
|
|
173
|
+
df: pd.DataFrame, report_name: Path, subbands: dict[str, tuple[int, int]]
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Generate an Excel report based on the provided DataFrame.
|
|
176
|
+
|
|
177
|
+
Parameters:
|
|
178
|
+
df (pd.DataFrame): The DataFrame to generate the report from.
|
|
179
|
+
report_name (Path): The name of the report file.
|
|
180
|
+
subbands (dict[str, tuple[int, int]]): A dictionary of subbands and their frequency ranges.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
None
|
|
184
|
+
"""
|
|
185
|
+
df_per_array = (
|
|
186
|
+
df.groupby(["Supplier", "Antenna_Model", "Revision_Version", "Array_ID"])[
|
|
187
|
+
"Cell"
|
|
188
|
+
]
|
|
189
|
+
.agg(["mean", "max"])
|
|
190
|
+
.reset_index()
|
|
191
|
+
.rename(columns={"mean": "Average", "max": "Max"})
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
df_per_array_per_tilt = df.pivot_table(
|
|
195
|
+
index=["Supplier", "Antenna_Model", "Revision_Version", "Array_ID"],
|
|
196
|
+
columns="Theta_Electrical_Tilt",
|
|
197
|
+
values="Cell",
|
|
198
|
+
aggfunc="mean",
|
|
199
|
+
)
|
|
200
|
+
df_per_array_per_tilt["Average"] = df_per_array_per_tilt.mean(axis=1)
|
|
201
|
+
df_per_array_per_tilt = df_per_array_per_tilt.reset_index()
|
|
202
|
+
|
|
203
|
+
avg_df_list = []
|
|
204
|
+
for subband_str, subband_freqs in subbands.items():
|
|
205
|
+
df_tmp = df[
|
|
206
|
+
df["Frequency_value"].between(subband_freqs[0], subband_freqs[1])
|
|
207
|
+
].copy()
|
|
208
|
+
if df_tmp.empty:
|
|
209
|
+
continue
|
|
210
|
+
df_tmp.loc[:, "Subband"] = subband_str
|
|
211
|
+
pivot_df = df_tmp.pivot_table(
|
|
212
|
+
index=[
|
|
213
|
+
"Supplier",
|
|
214
|
+
"Antenna_Model",
|
|
215
|
+
"Revision_Version",
|
|
216
|
+
"Array_ID",
|
|
217
|
+
"Subband",
|
|
218
|
+
],
|
|
219
|
+
columns="Theta_Electrical_Tilt",
|
|
220
|
+
values="Cell",
|
|
221
|
+
aggfunc="mean",
|
|
222
|
+
)
|
|
223
|
+
pivot_df["Average"] = pivot_df.mean(axis=1)
|
|
224
|
+
avg_df_list.append(pivot_df.reset_index())
|
|
225
|
+
df_per_arrayandsubband_per_tilt = pd.concat(avg_df_list, ignore_index=True)
|
|
226
|
+
|
|
227
|
+
with pd.ExcelWriter(report_name) as writer:
|
|
228
|
+
df.to_excel(writer, index=False, sheet_name="Raw_Data")
|
|
229
|
+
df_per_array.to_excel(writer, index=False, sheet_name="Mean_Max_Per_ArrayID")
|
|
230
|
+
df_per_array_per_tilt.to_excel(
|
|
231
|
+
writer, index=False, sheet_name="Mean_ArrayID_Tilt"
|
|
232
|
+
)
|
|
233
|
+
df_per_arrayandsubband_per_tilt.to_excel(
|
|
234
|
+
writer, index=False, sheet_name="Mean_ArrayID_Subband_Tilt"
|
|
235
|
+
)
|
|
236
|
+
logging.info("Report: ✨Generated EAS BE report in %s✨", report_name)
|
|
237
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|