Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
71529f0
refactor: extract NGMN metadata accessors into ngmn package (Phase 2a)
tiamilani Sep 3, 2026
bd2a539
refactor: move unit-conversion metadata into ngmn.Metadata (Phase 2b)
tiamilani Sep 3, 2026
5004380
refactor: move sampling/dataframe metadata to ngmn.Metadata, deprecat…
tiamilani Sep 3, 2026
d508081
refactor: extract directivity and quadrature into metrics package (Ph…
tiamilani Sep 3, 2026
8b81c3c
refactor: extract peak and top-3dB metrics into metrics.peak (Phase 3b)
tiamilani Sep 3, 2026
f1415c5
refactor: extract beam efficiency into metrics.efficiency, drop erido…
tiamilani Sep 3, 2026
e20ef2b
fix: do not emit our own deprecation warning while loading a pattern
tiamilani Sep 3, 2026
be116c6
refactor: extract coordinate transforms into ngmn.coordinates (Phase 4a)
tiamilani Sep 3, 2026
0a95352
refactor: extract JSON loading and validation into ngmn.loader (Phase…
tiamilani Sep 3, 2026
e04e081
refactor: move pattern processing into _processing.py (Phase 4c)
tiamilani Sep 3, 2026
e1ac766
feat!: harden schema provenance/versioning and NGMN metadata contracts
tiamilani Sep 7, 2026
c3168a2
refactor: extract json_load + guards, rename data/pattern attrs with …
tiamilani Sep 8, 2026
27445d8
refactor(processing): decompose _process_pattern_data into focused steps
tiamilani Sep 8, 2026
59076ab
refactor(sector): split sector_definitions into a sector package
tiamilani Sep 8, 2026
97180dd
refactor: adopt the verify() guard in losses and the NGMN preset
tiamilani Sep 8, 2026
cfcb59d
refactor(efficiency): adopt verify() guards and migrate to BoundaryBox
tiamilani Sep 8, 2026
30b3dc6
refactor(peak): adopt verify() guard and rename pattern_3d param
tiamilani Sep 8, 2026
41bbcee
refactor(ngmn): adopt verify() guards and extract unit converters
tiamilani Sep 8, 2026
1a231f8
docs(changelog): refresh for the sector package, guards, and json_load
tiamilani Sep 8, 2026
a10c2d0
Added a README with difference between sectors configurations
tiamilani Sep 8, 2026
f2b415c
Docs, added comment on the transpose operation in quadrature
tiamilani Sep 16, 2026
0d6f00c
docs and notebooks examples update, improved error message in report.py
tiamilani Sep 16, 2026
2cbdae7
Merge remote-tracking branch 'origin/main' into refactor/split-parser…
tiamilani Sep 16, 2026
4fbe3a7
refactor(deprecations)!: emit FutureWarning on every deprecated entry…
tiamilani Sep 16, 2026
2af4e03
docs(notebooks): migrate examples off deprecated SectorDefinition
tiamilani Sep 16, 2026
da864f7
test: align suite with FutureWarning deprecations and enforce them
tiamilani Sep 16, 2026
c21db69
Fix(notebooks): fixing the example report notebook and adding a new n…
tiamilani Sep 23, 2026
1acc8fc
Fix(ci): removed image comparison tests for now due to a behaviour mi…
tiamilani Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ jobs:
pip install poetry poetry-dynamic-versioning
poetry install --only main,dev
- name: Pytest
run: poetry run pytest
# Baseline image tests excluded: plotly/kaleido renders differ across
# machines (Chromium build + fonts), causing false failures. See plan.md
# "Understand cross-machine image baseline instability" (ties to §2.7 / M2-13).
run: poetry run pytest -m "not image"

build:
name: Build (Python ${{ matrix.python-version }})
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,5 @@ plan.md
# transient artifacts from baseline image comparison (tests/test_plot.py)
tests/fixtures/*-actual.png
tests/fixtures/*-failed-diff.png
.trash/
trashcan/
109 changes: 102 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- This changelog.
- Test coverage for the metadata properties, the two plotting methods, and the directivity
calculations (78 tests), taking the suite from 44 to 122 and `src` coverage to 81%.
calculations, plus dedicated tests added alongside the parser decomposition (coordinates,
loader, normalization, non-uniform loading, processing, efficiency, peak). The suite grew
from 44 to **228 tests** and `src` statement coverage to **88%**.
Covers metadata unit conversion, the required/optional key split, the `plot()` /
`plot_3D()` return contract, and — for the plots — which data array is drawn on which
axis, with which colour range and scale.
`plot_3D()` returned items , and which data array is drawn on which axis, with which
colour range and scale.
- Correctness tests for `calculate_directivity()` and `calculate_losses()`, which were
previously at 8% and 20% statement coverage and are now fully covered. Directivity is
verified against closed-form analytical values for isotropic, `sin^2`, `sin^4` and
Expand All @@ -31,25 +33,118 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
environments, so they can be excluded with `pytest -m "not image"`.
- Development dependencies: `jupyterlab`, `matplotlib` and `pytest-mpl` (test-only, for
image comparison), and `pytest-cov`. None of these affect the published wheel.
- A `verify()` guard helper in `eas_3d_pattern.util_func.guards`: a single check-and-raise
utility (log an error and raise a chosen exception when a condition is falsy) that
replaces the repeated inline `if ...: logger.error(...); raise ...` blocks across the
parser, processing, metrics, coordinates and sector modules. Covered by `test_guards.py`.
- `json_load()` in `eas_3d_pattern.ngmn.loader` (also re-exported from `eas_3d_pattern.ngmn`):
reads a pattern file and normalizes vendor-specific keys in one call, composed from the
pure `load_json_file()` and `normalize_keys()` helpers.

### Changed

- **BREAKING** — Corrected the spelling of two misspelled public properties on
`AntennaPattern`. No deprecation aliases are provided; the old names are removed
outright.
- The sector code moved from the single `sector_definitions.py` module into a
`sector/` package: `sector.definitions` (the `BoundaryBox` region and the `Sector`
collection), `sector.presets` (the `SectorPreset` base and the `from_preset` /
`preset_names` / `validate_preset` registry helpers), and one module per preset
(`sector.eas_preset.EasPreset`, `sector.ngmn_type_a_preset.NgmnTypeAPreset`). Presets
are now `SectorPreset` subclasses with a `load()` method rather than free builder
functions. The public names (`SectorDefinition`, `BoundaryBoxSquare`) remain importable
from `eas_3d_pattern` unchanged; `BoundaryBox`, `Sector`, and the preset helpers are new
public names exported from `eas_3d_pattern.sector`.

- Internal refactor (no public API change): the `AntennaPattern` god class was decomposed.
Logic moved out of `parser.py` (1140 → 422 lines) into `ngmn/` (`loader`, `coordinates`,
`metadata`) and `metrics/` (`directivity`, `efficiency`, `peak`, `quadrature`) sub-packages,
plus `processing.py` and `plotting.py`. The public import surface (`AntennaPattern`,
`SectorDefinition`, `NGMNSchema`, `SAMPLE_JSON`, `generate_report_eas`) is unchanged.

- **BREAKING** — `AntennaPattern(data_filepath=...)` now accepts `str | pathlib.Path`
(previously `str` only), and the stored `AntennaPattern.data_filepath` attribute is now a
`pathlib.Path` regardless of the input type (previously the raw `str` was passed). Code that
passed a `str` continues to work; code that read `pattern.data_filepath` and relied on it
being a `str` (e.g. calling `str`-only methods on it) must wrap it in `str(...)` or use the
`Path` API. Internally, `os.path` was replaced by `pathlib` in `__init__` and `__str__`.

- **BREAKING** — Corrected the spelling of two public properties on `AntennaPattern`.
No deprecation aliases are provided; the old names are removed.

| Old name | New name |
|----------|----------|
| `AntennaPattern.phi_eletrical_pan` | `AntennaPattern.phi_electrical_pan` |
| `AntennaPattern.theta_eletrical_tilt` | `AntennaPattern.theta_electrical_tilt` |

Migration: rename any attribute access in your own code. The underlying NGMN JSON
Migration: rename any attribute access. The underlying NGMN JSON
keys (`Phi_Electrical_Pan`, `Theta_Electrical_Tilt`) were always spelled correctly
and are unaffected, as is the output of `print(pattern)`.

- `AntennaPattern.plot()` and `AntennaPattern.plot_3D()` return annotations are now
`go.Figure | None` (was `None | go.Figure`). No behavioural change.
- Reworded a `calculate_directivity()` docstring note for clarity.
- Renamed the loaded-payload attribute `AntennaPattern.raw_data` to
`AntennaPattern.data`. The payload is normalized on load (vendor key variants are
mapped to their canonical NGMN names). The old name is preserved as a deprecated
alias (see *Deprecated*), so existing code keeps working; migrate attribute access
from `pattern.raw_data` to `pattern.data`.
- Renamed the processed-dataset attribute `AntennaPattern.Pattern_3D` to
`AntennaPattern.pattern` (PEP 8 lowercase, dropped the redundant `_3D` suffix). The
old name is preserved as a deprecated alias (see *Deprecated*); migrate attribute
access from `antenna.Pattern_3D` to `antenna.pattern`.

### Removed

- The internal Ericsson document link from the `calculate_beam_efficiency()` docstring.
It pointed at an eridoc URL not reachable from the outside and exposed an internal
document identifier in a public repository.
The EAS beam-efficiency methodology will be documented publicly instead.

### Deprecated

- `BoundaryBoxSquare` is renamed to `BoundaryBox` (the "Square" suffix was
redundant). `BoundaryBoxSquare` remains as an alias for backward compatibility and
is scheduled for removal in a future release; use `BoundaryBox` instead. The class
also no longer carries a `name` field: a box is a pure geometry value object, and
the owning `Sector`/`SectorDefinition` names it via its dict key.

- The `SectorDefinition(load_default=..., top_border=...)` constructor path is
deprecated in favour of the preset builder
`SectorDefinition.from_preset("eas", top_border=...)` (for the EAS sectors) or
`SectorDefinition(load_default=False)` (for an empty instance). **This is not a
breaking change: the constructor still works exactly as before.** `load_default`
still defaults to `True` and still requires `top_border`, so existing calls keep
their current behaviour; they now additionally emit a `DeprecationWarning`
pointing at the replacement. The EAS sector geometry has moved out of
`SectorDefinition` into the `eas` preset builder, and the deprecated path simply
delegates to it, so the loaded sectors are identical. The `load_default` /
`top_border` parameters are scheduled for removal in a future release, at which
point `SectorDefinition()` will construct an empty instance.

```python
# Deprecated — emits a DeprecationWarning (still works):
sectors = SectorDefinition(load_default=True, top_border=85.0) # EAS sectors
sectors = SectorDefinition(load_default=False) # empty instance

# New way:
from eas_3d_pattern.sector import from_preset, Sector

sectors = from_preset("eas", top_border=85.0) # EAS sectors
sectors = Sector() # empty instance

# Still supported (SectorDefinition classmethod delegates to the preset registry):
sectors = SectorDefinition.from_preset("eas", top_border=85.0)
```
- `AntennaPattern.Pattern_3D` is now a deprecated read-only alias for
`AntennaPattern.pattern` and emits a `DeprecationWarning`. It returns the same
`xarray.Dataset` object as `pattern` (not a copy). Scheduled for removal in a
future release; use `antenna.pattern` instead.
- `AntennaPattern.raw_data` is now a deprecated read-only alias for
`AntennaPattern.data` and emits a `DeprecationWarning`. It returns the same
dictionary object as `data` (not a copy), so in-place mutation through the alias
still reaches the underlying payload. Scheduled for removal in a future release;
use `pattern.data` instead.
- `AntennaPattern.is_nonuniform_sampling` now emits a `DeprecationWarning` and is
scheduled for removal in a future release. It is exactly the negation of
`is_uniform_sampling`; use `not pattern.is_uniform_sampling` instead.

### Fixed

Expand Down
16 changes: 15 additions & 1 deletion notebooks/Example_00_Installation_Google_Colab.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,22 @@
}
],
"metadata": {
"kernelspec": {
"display_name": "eas-3d-pattern-bLjU7ZbS-py3.13",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.15"
}
},
"nbformat": 4,
Expand Down
153 changes: 6 additions & 147 deletions notebooks/Example_01_NGMNSchema.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -29,161 +29,20 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": null,
"id": "f63e5e3d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<SchemaManager(id=0x2210f177620, schema_loaded=True, source=Downloaded from URL (https://www.ngmn.org/schema/basta/NGMN_BASTA_AA_3drp_JSON_Schema_WP3_0_latest.json))>"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"NGMNSchema"
]
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": null,
"id": "73dcf58a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'$schema': 'http://json-schema.org/draft/2020-12/schema#',\n",
" 'type': 'object',\n",
" 'properties': {'BASTA_AA_WP_version': {'type': 'string'},\n",
" 'Supplier': {'type': 'string'},\n",
" 'Antenna_Model': {'type': 'string'},\n",
" 'Antenna_Type': {'type': 'string'},\n",
" 'Revision_Version': {'type': 'string'},\n",
" 'Released_Date': {'type': 'string', 'format': 'date'},\n",
" 'Coordinate_System': {'type': 'string',\n",
" 'enum': ['SPCS_Polar', 'SPCS_CW', 'SPCS_CCW', 'SPCS_Geo']},\n",
" 'Pattern_Name': {'type': ['string', 'null']},\n",
" 'Beam_ID': {'type': ['string', 'null']},\n",
" 'Pattern_Type': {'type': 'string',\n",
" 'enum': ['Broadcast Beam',\n",
" 'Broadcast Configuration Envelope',\n",
" 'Traffic Beam',\n",
" 'Traffic Envelope',\n",
" 'Uplink',\n",
" 'Generic']},\n",
" 'Frequency': {'type': 'object',\n",
" 'properties': {'value': {'type': 'number', 'minimum': 0},\n",
" 'unit': {'type': 'string', 'enum': ['Hz', 'kHz', 'MHz', 'GHz', 'THz']}},\n",
" 'required': ['value', 'unit']},\n",
" 'Frequency_Range': {'type': ['object', 'null'],\n",
" 'properties': {'lower': {'type': 'number', 'minimum': 0},\n",
" 'upper': {'type': 'number', 'minimum': 0},\n",
" 'unit': {'type': 'string', 'enum': ['Hz', 'kHz', 'MHz', 'GHz', 'THz']}},\n",
" 'required': ['lower', 'upper', 'unit']},\n",
" 'EIRP': {'type': ['object', 'null'],\n",
" 'properties': {'value': {'type': 'number'},\n",
" 'unit': {'type': 'string', 'enum': ['mW', 'W', 'dBW', 'dBm']}},\n",
" 'required': ['value', 'unit']},\n",
" 'Configured_Output_Power': {'type': ['object', 'null'],\n",
" 'properties': {'value': {'type': 'number'},\n",
" 'unit': {'type': 'string', 'enum': ['mW', 'W', 'dBW', 'dBm']}},\n",
" 'required': ['value', 'unit']},\n",
" 'Gain': {'type': ['object', 'null'],\n",
" 'properties': {'value': {'type': 'number'},\n",
" 'unit': {'type': 'string', 'enum': ['dBi', 'dBd']}},\n",
" 'required': ['value', 'unit']},\n",
" 'Configuration': {'type': ['string', 'null']},\n",
" 'RF_Port': {'type': ['string', 'null']},\n",
" 'Array_ID': {'type': ['string', 'null']},\n",
" 'Array_Position': {'type': ['string', 'null']},\n",
" 'Phi_HPBW': {'type': 'number', 'exclusiveMinimum': 0},\n",
" 'Theta_HPBW': {'type': 'number', 'exclusiveMinimum': 0},\n",
" 'Front_to_Back': {'type': 'number', 'exclusiveMinimum': 0},\n",
" 'Phi_Electrical_Pan': {'type': ['number', 'null']},\n",
" 'Theta_Electrical_Tilt': {'type': ['number', 'null']},\n",
" 'Nominal_Polarization': {'anyOf': [{'type': 'string',\n",
" 'enum': ['Total',\n",
" 'P45',\n",
" 'M45',\n",
" 'P45M45',\n",
" 'M45P45',\n",
" 'H',\n",
" 'V',\n",
" 'HV',\n",
" 'VH',\n",
" 'RHC',\n",
" 'LHC']},\n",
" {'$comment': 'M### or P###M### for Negative|Positive ### degrees (### is an integer, max. 3 digits)',\n",
" 'type': 'string',\n",
" 'pattern': '^(P[0-9]{1,3})?M[0-9]{1,3}$'},\n",
" {'$comment': 'P### or M###P### for Positive|Negative ### degrees (### is an integer, max. 3 digits)',\n",
" 'type': 'string',\n",
" 'pattern': '^(M[0-9]{1,3})?P[0-9]{1,3}$'}]},\n",
" 'Theta_Sampling': {'type': ['array', 'null'],\n",
" 'minItems': 3,\n",
" 'maxItems': 3,\n",
" 'items': {'type': 'number'}},\n",
" 'Phi_Sampling': {'type': ['array', 'null'],\n",
" 'minItems': 3,\n",
" 'maxItems': 3,\n",
" 'items': {'type': 'number'}},\n",
" 'Data_Set_Row_Structure': {'type': 'array',\n",
" 'minItems': 1,\n",
" 'maxItems': 7,\n",
" 'items': {'type': 'string',\n",
" 'enum': ['Theta',\n",
" 'Phi',\n",
" 'MagAttenuationTP',\n",
" 'MagAttenuationCo',\n",
" 'MagAttenuationCr',\n",
" 'PhaseCo',\n",
" 'PhaseCr']}},\n",
" 'Data_Set': {'type': 'array',\n",
" 'items': {'type': 'array',\n",
" 'minItems': 1,\n",
" 'maxItems': 7,\n",
" 'items': {'type': 'number'}}}},\n",
" 'required': ['BASTA_AA_WP_version',\n",
" 'Supplier',\n",
" 'Antenna_Model',\n",
" 'Antenna_Type',\n",
" 'Revision_Version',\n",
" 'Released_Date',\n",
" 'Coordinate_System',\n",
" 'Pattern_Name',\n",
" 'Beam_ID',\n",
" 'Pattern_Type',\n",
" 'Frequency',\n",
" 'Frequency_Range',\n",
" 'EIRP',\n",
" 'Configured_Output_Power',\n",
" 'Gain',\n",
" 'Configuration',\n",
" 'RF_Port',\n",
" 'Array_ID',\n",
" 'Array_Position',\n",
" 'Phi_HPBW',\n",
" 'Theta_HPBW',\n",
" 'Front_to_Back',\n",
" 'Phi_Electrical_Pan',\n",
" 'Theta_Electrical_Tilt',\n",
" 'Nominal_Polarization',\n",
" 'Theta_Sampling',\n",
" 'Phi_Sampling',\n",
" 'Data_Set_Row_Structure',\n",
" 'Data_Set']}"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"NGMNSchema.schema_content"
]
Expand Down Expand Up @@ -212,7 +71,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "eas-3d-pattern-GaKWT4X0-py3.13",
"display_name": "eas-3d-pattern-bLjU7ZbS-py3.13",
"language": "python",
"name": "python3"
},
Expand All @@ -226,7 +85,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.3"
"version": "3.13.15"
}
},
"nbformat": 4,
Expand Down
Loading
Loading