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.
Files changed (17) hide show
  1. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/PKG-INFO +5 -3
  2. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/pyproject.toml +3 -2
  3. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/__init__.py +5 -2
  4. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/parser.py +209 -2
  5. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/util_func/report.py +37 -1
  6. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/LICENSE +0 -0
  7. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/README.md +0 -0
  8. {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
  9. {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
  10. {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
  11. {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
  12. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sample_data/__init__.py +0 -0
  13. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/schema_manager.py +0 -0
  14. {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
  15. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/schemas/__init__.py +0 -0
  16. {eas_3d_pattern-0.1.2 → eas_3d_pattern-0.1.3}/src/eas_3d_pattern/sector_definitions.py +0 -0
  17. {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.3
1
+ Metadata-Version: 2.4
2
2
  Name: eas-3d-pattern
3
- Version: 0.1.2
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@ericsson.com
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@ericsson.com"}
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.2"
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 radation pattern as heatmap.
787
+ """Plots the radiation pattern as heatmap.
713
788
 
714
- Plots with ploty the radation pattern as heatmap.
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("*.json"))
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