eas-3d-pattern 0.1.2__tar.gz → 0.1.3__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.2 → eas_3d_pattern-0.1.3}/PKG-INFO +5 -3
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/pyproject.toml +3 -2
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/__init__.py +5 -2
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/parser.py +209 -2
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/util_func/report.py +37 -1
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/LICENSE +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/README.md +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL1_2100_A-Y1_RFp01_T00p0_P45_NonUniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL2_2100_A-Y1_RFp01_T00p0_P45_UniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL3_3700_EID12345_T00p0_P45_NonUniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sample_data/VENDOR_ANTMODEL4_3700_BID67890_T00p0_P45_UniformSampling_3drp.json +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sample_data/__init__.py +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/schema_manager.py +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/schemas/NGMN_BASTA_AA_3drp_JSON_Schema_WP3_0_latest.json +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/schemas/__init__.py +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sector_definitions.py +0 -0
- {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/util_func/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: eas-3d-pattern
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A tool to visualize 3D antenna pattern files in JSON format.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -23,9 +23,10 @@ License: MIT License
|
|
|
23
23
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
24
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
25
|
SOFTWARE.
|
|
26
|
+
License-File: LICENSE
|
|
26
27
|
Keywords: antenna,pattern,3d pattern,visualization,NGMN,BASTA,RF,JSON
|
|
27
28
|
Author: Georg Polownikow
|
|
28
|
-
Author-email: georg.polownikow@
|
|
29
|
+
Author-email: georg.polownikow@gmail.com
|
|
29
30
|
Requires-Python: >=3.11,<3.14
|
|
30
31
|
Classifier: Development Status :: 3 - Alpha
|
|
31
32
|
Classifier: Intended Audience :: Telecommunications Industry
|
|
@@ -39,6 +40,7 @@ Classifier: Operating System :: OS Independent
|
|
|
39
40
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
40
41
|
Requires-Dist: jsonschema (>=4.24.0,<5.0.0)
|
|
41
42
|
Requires-Dist: kaleido (>=0.1.0,<0.2.0)
|
|
43
|
+
Requires-Dist: nbformat (>=4.2.0,<6.0.0)
|
|
42
44
|
Requires-Dist: numpy (>=2.0.0,<3.0.0)
|
|
43
45
|
Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
|
|
44
46
|
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
@@ -3,7 +3,7 @@ name = "eas-3d-pattern"
|
|
|
3
3
|
dynamic = []
|
|
4
4
|
description = "A tool to visualize 3D antenna pattern files in JSON format."
|
|
5
5
|
authors = [
|
|
6
|
-
{name = "Georg Polownikow",email = "georg.polownikow@
|
|
6
|
+
{name = "Georg Polownikow",email = "georg.polownikow@gmail.com"}
|
|
7
7
|
]
|
|
8
8
|
license = {file = "LICENSE"}
|
|
9
9
|
readme = "README.md"
|
|
@@ -20,6 +20,7 @@ dependencies = [
|
|
|
20
20
|
"kaleido (>=0.1.0, <0.2.0)",
|
|
21
21
|
"requests (>=2.32.2,<3.0.0)",
|
|
22
22
|
"tqdm (>=4.67.1,<5.0.0)",
|
|
23
|
+
"nbformat (>=4.2.0,<6.0.0)",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
classifiers = [
|
|
@@ -34,7 +35,7 @@ classifiers = [
|
|
|
34
35
|
"Operating System :: OS Independent",
|
|
35
36
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
36
37
|
]
|
|
37
|
-
version = "0.1.
|
|
38
|
+
version = "0.1.3"
|
|
38
39
|
|
|
39
40
|
[project.urls]
|
|
40
41
|
homepage = "https://www.ericsson.com/antenna-system"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
3
|
|
|
3
4
|
from .parser import AntennaPattern
|
|
4
5
|
from .sample_data import SAMPLE_JSON
|
|
@@ -10,6 +11,10 @@ LIBRARY_PACKAGE_NAME = __name__
|
|
|
10
11
|
library_root_logger = logging.getLogger(LIBRARY_PACKAGE_NAME)
|
|
11
12
|
if not library_root_logger.hasHandlers():
|
|
12
13
|
library_root_logger.addHandler(logging.NullHandler())
|
|
14
|
+
try:
|
|
15
|
+
__version__ = version(LIBRARY_PACKAGE_NAME)
|
|
16
|
+
except PackageNotFoundError:
|
|
17
|
+
__version__ = "0+unknown"
|
|
13
18
|
|
|
14
19
|
__all__ = [
|
|
15
20
|
"SAMPLE_JSON",
|
|
@@ -18,5 +23,3 @@ __all__ = [
|
|
|
18
23
|
"SectorDefinition",
|
|
19
24
|
"generate_report_eas",
|
|
20
25
|
]
|
|
21
|
-
|
|
22
|
-
__version__ = "0.1.1"
|
|
@@ -505,6 +505,81 @@ class AntennaPattern:
|
|
|
505
505
|
metadata.pop("Data_Set_Row_Structure", None)
|
|
506
506
|
return metadata
|
|
507
507
|
|
|
508
|
+
def calculate_directivity(self) -> float:
|
|
509
|
+
"""Calculate the directivity of the antenna pattern data.
|
|
510
|
+
|
|
511
|
+
Directivity is calculated with the average radiation intensity over the whole sphere and the maximum radiation intensity.
|
|
512
|
+
Can only be calculated if data is complete (full sphere). Regular grids are advised.
|
|
513
|
+
Fancy geometry calculation with something like Delaunay+Voronoi is not supported for now.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
None
|
|
517
|
+
|
|
518
|
+
Raises:
|
|
519
|
+
None
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
float: The directivity value in dBi.
|
|
523
|
+
|
|
524
|
+
Example:
|
|
525
|
+
>>> directivity_dbi = antenna_pattern.calculate_directivity()
|
|
526
|
+
>>> losses = gain_dbi - directivity_dbi
|
|
527
|
+
"""
|
|
528
|
+
logger.debug("AntennaPattern: Calculating directivity of antenna pattern data.")
|
|
529
|
+
if "dOmega" not in list(self.Pattern_3D.data_vars.keys()):
|
|
530
|
+
weight = np.repeat(
|
|
531
|
+
np.sin(np.deg2rad(self.Pattern_3D.Theta.values)).T[:, None],
|
|
532
|
+
len(self.Pattern_3D.Phi),
|
|
533
|
+
axis=1,
|
|
534
|
+
)
|
|
535
|
+
dTheta = np.abs(np.gradient(np.deg2rad(self.Pattern_3D["Theta"]))).reshape(
|
|
536
|
+
-1, 1
|
|
537
|
+
)
|
|
538
|
+
dPhi = np.abs(np.gradient(np.deg2rad(self.Pattern_3D["Phi"]))).reshape(
|
|
539
|
+
1, -1
|
|
540
|
+
)
|
|
541
|
+
factor = dTheta * dPhi
|
|
542
|
+
dOmega = weight * factor
|
|
543
|
+
self.Pattern_3D["dOmega"] = xr.DataArray(
|
|
544
|
+
dOmega,
|
|
545
|
+
dims=("Theta", "Phi"),
|
|
546
|
+
coords={"Theta": self.Pattern_3D.Theta, "Phi": self.Pattern_3D.Phi},
|
|
547
|
+
name="dOmega",
|
|
548
|
+
)
|
|
549
|
+
Umax = float(self.Pattern_3D["P_tp_lin"].max())
|
|
550
|
+
Uavg = float(
|
|
551
|
+
(self.Pattern_3D["P_tp_lin"] * self.Pattern_3D["dOmega"]).sum(
|
|
552
|
+
("Theta", "Phi")
|
|
553
|
+
)
|
|
554
|
+
/ (self.Pattern_3D["dOmega"].sum())
|
|
555
|
+
)
|
|
556
|
+
directivity_dbi = float(10 * np.log10(Umax / Uavg))
|
|
557
|
+
return directivity_dbi
|
|
558
|
+
|
|
559
|
+
def calculate_losses(self) -> float:
|
|
560
|
+
"""Calculate the losses of the antenna pattern data.
|
|
561
|
+
|
|
562
|
+
Antenna losses are calculated by the gain value within the header and the calculated directivity.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
None
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
None
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
float: The loss value in dB (gain - directivity).
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
>>> losses = antenna_pattern.calculate_losses()
|
|
575
|
+
"""
|
|
576
|
+
logger.debug("AntennaPattern: Calculating losses of antenna pattern data.")
|
|
577
|
+
if self.gain_dbi is None:
|
|
578
|
+
raise ValueError(
|
|
579
|
+
"AntennaPattern: Loss can only be calculated if 'Gain' is available in the header"
|
|
580
|
+
)
|
|
581
|
+
return float(self.gain_dbi - self.calculate_directivity())
|
|
582
|
+
|
|
508
583
|
def calculate_beam_efficiency(
|
|
509
584
|
self, sector_definitions: SectorDefinition | None = None, powersum: bool = True
|
|
510
585
|
) -> dict[str, float]:
|
|
@@ -709,9 +784,9 @@ class AntennaPattern:
|
|
|
709
784
|
def plot(
|
|
710
785
|
self, component_name: str = "P_tp_dB", show_fig: bool = True
|
|
711
786
|
) -> None | go.Figure:
|
|
712
|
-
"""Plots the
|
|
787
|
+
"""Plots the radiation pattern as heatmap.
|
|
713
788
|
|
|
714
|
-
Plots with
|
|
789
|
+
Plots with plotly the radiation pattern as heatmap.
|
|
715
790
|
Normalized radation pattern are shown in dB.
|
|
716
791
|
|
|
717
792
|
Args:
|
|
@@ -783,6 +858,138 @@ class AntennaPattern:
|
|
|
783
858
|
return None
|
|
784
859
|
return fig
|
|
785
860
|
|
|
861
|
+
def plot_3D(
|
|
862
|
+
self,
|
|
863
|
+
component_name: str = "P_tp_dB",
|
|
864
|
+
db_floor: float = -30.0,
|
|
865
|
+
show_axes_arrows: bool = True,
|
|
866
|
+
show_fig: bool = True,
|
|
867
|
+
) -> None | go.Figure:
|
|
868
|
+
"""Plots the radiation pattern as 3D polar plot.
|
|
869
|
+
|
|
870
|
+
Plots with plotly the radiation pattern as 3D polar plot.
|
|
871
|
+
dB_floor is set to -30 as average baseline.
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
component_name (str, optional): Name of the component to be plotted. Defaults to 'P_tp_dB', thus power pattern.
|
|
875
|
+
db_floor (float, optional): Sets a minimum floor to have smoother plots. Defaults to -30.
|
|
876
|
+
show_axes_arrows (bool, optional): Shows coordinate axes arrows in the plot. Defaults to True.
|
|
877
|
+
show_fig (bool, optional): Whether to show the figure. Defaults to True.
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
None | go.Figure: None if show_fig is False else go.Figure
|
|
881
|
+
|
|
882
|
+
"""
|
|
883
|
+
if component_name not in self.Pattern_3D.data_vars:
|
|
884
|
+
logger.error(
|
|
885
|
+
f"AntennaPattern: Component '{component_name}' not found. Make sure to select a component with data."
|
|
886
|
+
)
|
|
887
|
+
raise ValueError(
|
|
888
|
+
f"AntennaPattern: Component '{component_name}' not found. Make sure to select a component with data."
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
theta_rad = np.radians(self.Pattern_3D.coords["Theta"].values)
|
|
892
|
+
phi_rad = np.radians(self.Pattern_3D.coords["Phi"].values)
|
|
893
|
+
theta_grid, phi_grid = np.meshgrid(theta_rad, phi_rad, indexing="ij")
|
|
894
|
+
r = self.Pattern_3D[component_name].values
|
|
895
|
+
r_clipped = np.clip(r, db_floor, r.max())
|
|
896
|
+
r_clipped -= r_clipped.min()
|
|
897
|
+
X = r_clipped * np.sin(theta_grid) * np.cos(phi_grid)
|
|
898
|
+
Y = r_clipped * np.sin(theta_grid) * np.sin(phi_grid)
|
|
899
|
+
Z = r_clipped * np.cos(theta_grid)
|
|
900
|
+
|
|
901
|
+
fig = go.Figure()
|
|
902
|
+
fig.add_trace(
|
|
903
|
+
go.Surface(
|
|
904
|
+
x=X,
|
|
905
|
+
y=Y,
|
|
906
|
+
z=Z,
|
|
907
|
+
surfacecolor=r,
|
|
908
|
+
colorscale="turbo",
|
|
909
|
+
cmin=db_floor,
|
|
910
|
+
cmax=0,
|
|
911
|
+
colorbar={"title": component_name, "thickness": 9},
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
fig.update_layout(
|
|
915
|
+
title={
|
|
916
|
+
"text": os.path.basename(self.data_filepath),
|
|
917
|
+
"x": 0.5,
|
|
918
|
+
"y": 0.93,
|
|
919
|
+
"yanchor": "bottom",
|
|
920
|
+
"font": {"size": 16},
|
|
921
|
+
},
|
|
922
|
+
height=650,
|
|
923
|
+
margin={"t": 50, "l": 0, "r": 0, "b": 0},
|
|
924
|
+
scene={
|
|
925
|
+
"aspectmode": "data",
|
|
926
|
+
"xaxis_title": "x",
|
|
927
|
+
"yaxis_title": "y",
|
|
928
|
+
"zaxis_title": "z",
|
|
929
|
+
},
|
|
930
|
+
)
|
|
931
|
+
if show_axes_arrows:
|
|
932
|
+
L = r_clipped.max() + 6
|
|
933
|
+
fig.add_trace(
|
|
934
|
+
go.Scatter3d(
|
|
935
|
+
x=[0, L],
|
|
936
|
+
y=[0, 0],
|
|
937
|
+
z=[0, 0],
|
|
938
|
+
mode="lines",
|
|
939
|
+
line={"color": "black", "width": 6},
|
|
940
|
+
showlegend=False,
|
|
941
|
+
hoverinfo="skip",
|
|
942
|
+
hovertemplate=None,
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
fig.add_trace(
|
|
946
|
+
go.Scatter3d(
|
|
947
|
+
x=[0, 0],
|
|
948
|
+
y=[0, L / 2],
|
|
949
|
+
z=[0, 0],
|
|
950
|
+
mode="lines",
|
|
951
|
+
line={"color": "black", "width": 6},
|
|
952
|
+
showlegend=False,
|
|
953
|
+
hoverinfo="skip",
|
|
954
|
+
hovertemplate=None,
|
|
955
|
+
)
|
|
956
|
+
)
|
|
957
|
+
fig.add_trace(
|
|
958
|
+
go.Scatter3d(
|
|
959
|
+
x=[0, 0],
|
|
960
|
+
y=[0, 0],
|
|
961
|
+
z=[0, L / 2],
|
|
962
|
+
mode="lines",
|
|
963
|
+
line={"color": "black", "width": 6},
|
|
964
|
+
showlegend=False,
|
|
965
|
+
hoverinfo="skip",
|
|
966
|
+
hovertemplate=None,
|
|
967
|
+
)
|
|
968
|
+
)
|
|
969
|
+
fig.add_trace(
|
|
970
|
+
go.Cone(
|
|
971
|
+
x=[L, 0, 0],
|
|
972
|
+
y=[0, L / 2, 0],
|
|
973
|
+
z=[0, 0, L / 2],
|
|
974
|
+
u=[4, 0, 0],
|
|
975
|
+
v=[0, 4, 0],
|
|
976
|
+
w=[0, 0, 4],
|
|
977
|
+
anchor="tail",
|
|
978
|
+
showscale=False,
|
|
979
|
+
autocolorscale=False,
|
|
980
|
+
sizemode="absolute",
|
|
981
|
+
sizeref=0.3,
|
|
982
|
+
colorscale=[[0, "black"], [1, "black"]],
|
|
983
|
+
showlegend=False,
|
|
984
|
+
hoverinfo="skip",
|
|
985
|
+
hovertemplate=None,
|
|
986
|
+
)
|
|
987
|
+
)
|
|
988
|
+
if show_fig:
|
|
989
|
+
fig.show()
|
|
990
|
+
return None
|
|
991
|
+
return fig
|
|
992
|
+
|
|
786
993
|
def __str__(self) -> str:
|
|
787
994
|
buf = io.StringIO()
|
|
788
995
|
self.raw_pattern_dataframe.info(buf=buf)
|
|
@@ -71,7 +71,7 @@ def generate_report_eas(
|
|
|
71
71
|
... ) # with own subband
|
|
72
72
|
"""
|
|
73
73
|
input_directory = Path(input_directory)
|
|
74
|
-
files = list(input_directory.glob("*.
|
|
74
|
+
files = list(input_directory.glob("*.[jJ][sS][oO][nN]"))
|
|
75
75
|
if len(files) == 0:
|
|
76
76
|
raise ValueError("Report: No .json files found in the directory.")
|
|
77
77
|
output_directory = Path(output_directory)
|
|
@@ -115,6 +115,42 @@ def _process_a_file(
|
|
|
115
115
|
data = pattern.get_metadata_dict()
|
|
116
116
|
top_border = pattern.calculate_top_3db_point(power=False)
|
|
117
117
|
eas_sectors = SectorDefinition(load_default=True, top_border=top_border)
|
|
118
|
+
if (data["Phi_HPBW"] <= 50) & (pattern.Pattern_3D.peak_coordinates[1] < -20):
|
|
119
|
+
logging.info(
|
|
120
|
+
"Reporting: Identified dual beam antenna. Overwriting sectors to dual beam definition for reporting."
|
|
121
|
+
)
|
|
122
|
+
eas_sectors.add_sector(
|
|
123
|
+
name="Cell",
|
|
124
|
+
theta_min=(top_border, "<="),
|
|
125
|
+
theta_max=(165, "<="),
|
|
126
|
+
phi_min=(-60.0, "<="),
|
|
127
|
+
phi_max=(0, "<="),
|
|
128
|
+
)
|
|
129
|
+
eas_sectors.add_sector(
|
|
130
|
+
name="Int2",
|
|
131
|
+
theta_min=(top_border, "<="),
|
|
132
|
+
theta_max=(165, "<="),
|
|
133
|
+
phi_min=(0.0, "<"),
|
|
134
|
+
phi_max=(180.0, "<="),
|
|
135
|
+
)
|
|
136
|
+
if (data["Phi_HPBW"] <= 50) & (pattern.Pattern_3D.peak_coordinates[1] > 20):
|
|
137
|
+
logging.info(
|
|
138
|
+
"Reporting: Identified dual beam antenna. Changing sectors to dual beam definition for reporting."
|
|
139
|
+
)
|
|
140
|
+
eas_sectors.add_sector(
|
|
141
|
+
name="Cell",
|
|
142
|
+
theta_min=(top_border, "<="),
|
|
143
|
+
theta_max=(165, "<="),
|
|
144
|
+
phi_min=(0.0, "<="),
|
|
145
|
+
phi_max=(60.0, "<="),
|
|
146
|
+
)
|
|
147
|
+
eas_sectors.add_sector(
|
|
148
|
+
name="Int1",
|
|
149
|
+
theta_min=(top_border, "<="),
|
|
150
|
+
theta_max=(165, "<="),
|
|
151
|
+
phi_min=(-180.0, "<="),
|
|
152
|
+
phi_max=(0.0, "<"),
|
|
153
|
+
)
|
|
118
154
|
eff = pattern.calculate_beam_efficiency(sector_definitions=eas_sectors)
|
|
119
155
|
data.update({f"{k}": v * 100 for k, v in eff.items()})
|
|
120
156
|
data.update(
|
|
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
|
|
File without changes
|