Metadata-Version: 2.4
Name: pyqd-channel
Version: 0.2.3
Summary: Python port of the QuaDRiGa radio channel model: coverage maps and static drops (non-commercial licence)
License-Expression: LicenseRef-QuaDRiGa-NonCommercial
Keywords: channel-model,3gpp,38.901,propagation,coverage-map,radio
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE-QuaDRiGa.txt
License-File: NOTICE.md
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.10
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Requires-Dist: h5py>=3.8; extra == "test"
Provides-Extra: examples
Requires-Dist: matplotlib>=3.6; extra == "examples"
Dynamic: license-file

# pyqd-channel

A geometry-based stochastic radio channel model in pure Python, for **static
drops and coverage maps**. NumPy and SciPy are the only dependencies.

It is a Python port of the QuaDRiGa channel model (v2.8.1-0) — the static-drop
and coverage-map subset — and reproduces its numerical behaviour to the
precision recorded under Accuracy, below. It is not affiliated with, nor
endorsed by, the original authors.

> **Non-commercial use only.** Copyright, licence conditions and the record of
> modification are in `NOTICE.md` and
> `LICENSE-QuaDRiGa.txt`; read both before redistributing.

---

## Install

```bash
pip install pyqd-channel
```

From source:

```bash
python -m venv .venv
./.venv/bin/pip install -e ".[test]"
./.venv/bin/python -m pytest -q
```

Python ≥ 3.10, NumPy and SciPy to run the model; the test extra adds pytest
and h5py. The reference data the suite checks against is committed, so no
other tooling is needed.

## Quick start

```python
import numpy as np
from pyqd_channel.antenna import generate, rotate_pattern
from pyqd_channel.layout import power_map

ant = generate("3gpp-3d", 1, 1, 2e9)     # 3GPP sector element at 2 GHz
rotate_pattern(ant, 90, "y", [0], 1)     # point it at the ground

maps, x, y = power_map(
    "3GPP_38.901_UMa_LOS", ant,
    np.array([500.0, 500.0, 30.0]),      # tx position [m]
    2e9,                                  # carrier [Hz]
    0, 1000, 0, 1000,                     # grid bounds [m]
    sample_distance=10, rx_height=1.5, tx_power=20,   # dBm
)
P = maps[0].sum(axis=(2, 3))             # (n_y, n_x), linear mW
```

Power at specific points, without building a grid:

```python
from pyqd_channel.builder import get_los_coeff
from pyqd_channel.scenario import load_scenario

cfg = load_scenario("3GPP_38.901_UMa_LOS")
c = get_los_coeff(ant, generate("omni"), tx_3x1, rx_3xN, 2e9, cfg.plpar, cfg.scenpar)
power_mw = (np.abs(c) ** 2).sum(axis=(0, 1)) * 10 ** (0.1 * 20)
```

## Examples

The examples install with the package. To find them:

```bash
python -c "import pyqd_channel; print(pyqd_channel.examples_path())"
```

That prints a directory containing:

| | |
|---|---|
| `basics/` | One concept per script, ~40 lines each. Start here. |
| `walkthroughs/` | Longer guided examples, in order. |

Every script runs standalone. Copy one somewhere writable and edit it, or run
it in place:

```bash
cd "$(python -c 'import pyqd_channel; print(pyqd_channel.examples_path())')"
python basics/01_load_scenario.py
python walkthroughs/01_first_coverage_map.py     # needs matplotlib
```

The walkthroughs that plot need `matplotlib`: `pip install "pyqd-channel[examples]"`.

## What you get

| Module | Purpose |
|---|---|
| `scenario` | 91 scenario tables, `.conf` parser |
| `antenna` | `omni` and `3gpp-3d` antennas, interpolation, rotation |
| `builder` | 10 path-loss models, LOS geometry and coefficients |
| `layout` | Coverage power maps |
| `sos` | Spatially correlated random fields |
| `simpar`, `array_order` | Settings, column-major array and numeric semantics |

## Accuracy

Validated at every layer against reference output captured from QuaDRiGa
v2.8.1-0. That captured output is committed here, so the tests need nothing
beyond this repository and no licence for the original.

| Layer | Agreement |
|---|---|
| Scenario configs | exact (91 × 140 fields) |
| SOS tables | 0 ULP |
| Path loss | ~1 ULP (85,657 values) |
| Antenna patterns | bit-exact to ~1e-10 |
| LOS coefficients | ~1e-12 |
| Power maps | < 1 float32-eps |

`power_map` computes in float64, while the reference maps are computed in
single precision throughout. The last row of the table is therefore set by that
single-precision arithmetic, not by anything this computation does.

## Not included

Mobility, time evolution, small-scale fading, scatterers, drifting, channel
merging, the 3GPP baseline path, dual mobility, satellites, ray tracing, 13 of
15 antenna types, `'detailed'` power maps.

Each raises `NotImplementedError` saying what is missing. Nothing fails
silently. The one place a questionable number is returned rather than refused
is the unphysical satellite geometry described below, and it warns.

The coverage-map path never calls `init_sos` or `gen_parameters`, so none of the
small-scale-fading machinery is on it.

## Gotchas

- **Power is dBm in, milliwatts out.** Convert with `10*log10(P)`.
- **Maps are `(n_y, n_x)`, not `(n_x, n_y)`.** Index `P[iy, ix]`: row is y,
  column is x. Getting this backwards is invisible on a square grid, so test
  with `n_x != n_y`, where it shows up.
- **`rotate_pattern(-90, "y")` points the beam UP.** Use `+90` for down. The
  sign is easy to get backwards, it does not raise, and it costs about 30 dB
  toward the ground.
- **Angles are radians**, except `get_angles`, which returns degrees in
  `[-180, 180)`.

## Known quirks

Three defects in the behaviour the reference data encodes. Reproducing a defect is
sometimes the only way to match the reference data, so each one below is either
followed deliberately or refused with an explicit error — never papered over.
All three are pinned by a test.

| Where | What |
|---|---|
| `'sf'` power maps | The mode is a no-op, identical to `'quick'`. Not offered. |
| `satellite` path loss | Multi-carrier input broadcasts on the wrong axis. Refused for `nF > 1`. |
| `satellite` elevation | Elevation uses absolute Tx height, so `asin` can go complex. Followed, with a warning. |

## Numerical notes

Three things that cost real debugging time, and that anyone reimplementing this
code has to get right.

**Evaluation order matters.** Arithmetic chains are evaluated strictly left to
right, and factoring a constant out of one changes the float result:

```python
sos_freq * maxD_old / maxD_new        # correct
np.arange(-180, 181) * np.pi / 180    # correct: (x*pi)/180
np.arange(-180, 181) * (np.pi / 180)  # WRONG: changed 88 of 361 samples
```

**Mixed double/single promotes to double**, rounding once. Pre-rounding the
scalar to float32 is wrong.

**float32 storage is deliberate.** The `sos` generator holds five of its arrays
(`dist`, `acf`, `sos_freq`, `sos_amp`, `sos_phase`) in float32. Widening them to
float64 is "more accurate" and never reproduces the reference values. Note also
that the rounding happens *before* the square root — `sqrt(float32(2/L))`, not
`float32(sqrt(2/L))`; the two differ for `L = 500` only.


## Attribution

This package is a language translation of an existing channel model. Its
structure, its parameters and the research behind them are not this project's
work, and all credit for them belongs to the original authors — who, with the
copyright holders and their contact address, are named in
`NOTICE.md`, which is distributed with this package.

Copyright © 2011–2023 Fraunhofer-Gesellschaft zur Förderung der angewandten
Forschung e.V., acting on behalf of its Fraunhofer Heinrich Hertz Institute.
This is a third-party modified version, not endorsed by Fraunhofer.

**Cite the original model, not this port.** `NOTICE.md` carries the
two canonical references — the journal paper and the version-specific technical
documentation — along with the authoritative source to verify them against.

**This code was written with AI assistance** (Claude), working from the
QuaDRiGa reference implementation and its captured outputs rather than from
scratch. Every layer is validated against the committed reference data rather
than against the model's own claims; see `tests/`. Treat it as you would any
translated code — the tests are the warrant, not the provenance.

### Licence

Non-commercial only (§2a: "scientific, education or standardization
purposes"). Redistributions must keep the licence text (§2b) and offer source
(§2c). Modified versions must be renamed away from the original product name
and carry change notices (§2e) — see `NOTICE.md`. **No patent
licence is granted** (§3). Provided **AS IS** (§4).

The complete licence text is in `LICENSE-QuaDRiGa.txt`.
For commercial use, or for any terms beyond the above, contact the copyright
holder at the address given in `NOTICE.md`.
