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.
Files changed (17) hide show
  1. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/PKG-INFO +4 -1
  2. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/pyproject.toml +4 -1
  3. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/__init__.py +9 -2
  4. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/parser.py +20 -0
  5. eas_3d_pattern-0.1.2/src/eas_3d_pattern/util_func/__init__.py +0 -0
  6. eas_3d_pattern-0.1.2/src/eas_3d_pattern/util_func/report.py +237 -0
  7. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/LICENSE +0 -0
  8. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/README.md +0 -0
  9. {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
  10. {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
  11. {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
  12. {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
  13. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/sample_data/__init__.py +0 -0
  14. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/schema_manager.py +0 -0
  15. {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
  16. {eas_3d_pattern-0.1.0 → eas_3d_pattern-0.1.2}/src/eas_3d_pattern/schemas/__init__.py +0 -0
  17. {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.0
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.0"
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__ = ["SAMPLE_JSON", "AntennaPattern", "NGMNSchema", "SectorDefinition"]
14
+ __all__ = [
15
+ "SAMPLE_JSON",
16
+ "AntennaPattern",
17
+ "NGMNSchema",
18
+ "SectorDefinition",
19
+ "generate_report_eas",
20
+ ]
14
21
 
15
- __version__ = "0.1.0"
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]:
@@ -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