From ffd5d1689e2f9688d9cd41c398c501a38c70f9e0 Mon Sep 17 00:00:00 2001 From: PiotrPich2024 Date: Fri, 13 Mar 2026 18:58:04 +0100 Subject: [PATCH 1/5] Add documentation for types: scalars, lists, and NumPy arrays --- docs/python/types.md | 157 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/python/types.md diff --git a/docs/python/types.md b/docs/python/types.md new file mode 100644 index 0000000..02152ef --- /dev/null +++ b/docs/python/types.md @@ -0,0 +1,157 @@ +--- +sidebar_position: 5 +--- + +# Types: scalars, lists, NumPy arrays (and float32 precision) + +Most `pyamtrack` numerical functions are backed by C/C++ code and exposed through **nanobind**. +To make the API convenient, many functions accept: + +- Python scalars (`float`, `int`) +- Python sequences (`list`, sometimes nested lists) +- NumPy arrays (`numpy.ndarray` / `ndarray`) + +This page explains what you can pass, what output you should expect, and why you should **avoid `numpy.float32`** for numerically sensitive work. + +--- + +## 1. Numeric scalars + +In most places where a parameter is documented as `float` (or `float | int`), you can pass: + +- `float` (Python’s float is a C `double`) +- `int` (will be converted and used where appropriate) + +Example: + +```python +import pyamtrack + +pyamtrack.converters.beta_from_energy(150) # int OK +pyamtrack.converters.beta_from_energy(150.0) # float OK +``` + +Many wrapped functions internally convert scalar inputs to C++ `double` using `nb::cast(...)`. + +--- + +## 2. Lists (vectorized “element-wise” calls) + +Many multi-argument functions are wrapped so that you can pass **Python lists** and get **vectorized** results. +The wrapper checks whether an argument is a Python `list` and then applies the computation element-by-element. + +Example: + +```python +import pyamtrack + +energies = [50.0, 100.0, 150.0] +r = pyamtrack.stopping.electron_range(energies, material=1, model="tabata") +# typically returns a NumPy array of float64 results +``` + +### Broadcasting scalars against lists/arrays +If at least one argument is list/array, scalar arguments are broadcast to match the vector length. + +--- + +## 3. NumPy arrays / ndarrays + +### 3.1 Floating inputs (energies, continuous parameters, etc.) +For many numeric inputs (e.g. energies), you can pass a NumPy array. +Internally, wrappers cast ndarray inputs to a `double`-based view, i.e. values are consumed as **C++ `double`**. +Recommended: + +```python +import numpy as np +import pyamtrack + +energy = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) +r = pyamtrack.stopping.electron_range(energy, material=1, model="tabata") +``` + +### 3.2 Integer-coded inputs (IDs like `material` and `model`) +Some parameters are IDs (e.g. `material`, `model`) and accept: + +- scalar `int` +- Python list of ints / objects (depending on parameter) +- NumPy arrays **with integer dtype** + +The library checks integer dtypes explicitly: `int8/16/32/64` and `uint8/16/32/64`. +Example (material IDs as an int array): + +```python +import numpy as np +import pyamtrack + +energies = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) +materials = np.asarray([1, 1, 1], dtype=np.int32) # integer dtype required for ID arrays + +r = pyamtrack.stopping.electron_range(energies, material=materials, model="tabata") +``` + +If you pass a NumPy array for an ID parameter but it is **not** an integer dtype, some wrappers will reject it. + +--- + +## 4. 0-D arrays and 1-D arrays + +Some wrappers accept **0-D** or **1-D** arrays (for vectorized evaluation). +For certain vectorized wrappers, NumPy array inputs are required to be **1-D** (otherwise a `ValueError` is raised). + +If you have higher-dimensional arrays, flatten them explicitly if that matches your intent: + +```python +x = np.asarray(x) +x1d = x.reshape(-1) # or x.ravel() +``` + +--- + +## 5. IMPORTANT: avoid `numpy.float32` (precision loss) + +Even though `numpy.float32` values are *convertible* to C++ `double`, using float32 can silently reduce precision. + +### Why? +- `numpy.float32` stores only ~7 decimal digits of precision. +- When you pass a float32 into `pyamtrack`, it is converted and used as a C++ `double`. +- But conversion **cannot restore precision that was already lost** when the value was stored as float32. + +So if you start with float32 inputs (scalar or array), you may see differences compared to float64/Python-float inputs—especially in calculations sensitive to small relative errors. + +### What to do instead +Prefer: +- Python `float` (scalar) +- `numpy.float64` (arrays) + +Examples: + +```python +import numpy as np + +# Bad (may lose precision before conversion to C double) +x_bad = np.float32(0.1) +arr_bad = np.asarray([0.1, 0.2, 0.3], dtype=np.float32) + +# Good +x_good = float(0.1) +arr_good = np.asarray([0.1, 0.2, 0.3], dtype=np.float64) + +# If you receive float32 data, upcast early: +arr = np.asarray(arr_bad, dtype=np.float64) +``` + +### Rule of thumb +If a parameter represents a *continuous physical quantity* (energy, LET, stopping power inputs, etc.), +use **float64 / Python float** unless you have a strong reason not to. + +--- + +## 6. Notes on return types + +Many functions return: +- a Python `float` for scalar inputs +- a NumPy array (`float64`) for vectorized inputs +- and sometimes nested lists / arrays when using cartesian-product evaluation + +See the docstring of each function for the exact behavior (for example, `stopping.electron_range` describes returning either a scalar or a NumPy array depending on input). \ No newline at end of file From dc3dd1158115987cc239e3a08d027979fe43ad15 Mon Sep 17 00:00:00 2001 From: PiotrPich2024 Date: Fri, 13 Mar 2026 19:34:24 +0100 Subject: [PATCH 2/5] Made types.md more user friendly --- docs/python/types.md | 189 +++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 89 deletions(-) diff --git a/docs/python/types.md b/docs/python/types.md index 02152ef..7295296 100644 --- a/docs/python/types.md +++ b/docs/python/types.md @@ -2,156 +2,167 @@ sidebar_position: 5 --- -# Types: scalars, lists, NumPy arrays (and float32 precision) +# Input types (Python / NumPy) and numerical precision -Most `pyamtrack` numerical functions are backed by C/C++ code and exposed through **nanobind**. -To make the API convenient, many functions accept: +`pyamtrack` is a Python interface to the C/C++ **libamtrack** library. Many functions accept either a **single number** or a **set of numbers** (a Python list or a NumPy array). Internally, most continuous physical quantities are computed using C/C++ **double precision** (`double`). -- Python scalars (`float`, `int`) -- Python sequences (`list`, sometimes nested lists) -- NumPy arrays (`numpy.ndarray` / `ndarray`) - -This page explains what you can pass, what output you should expect, and why you should **avoid `numpy.float32`** for numerically sensitive work. +This page explains what you can pass to `pyamtrack` functions and how to avoid the most common numerical pitfalls—especially when using `numpy.float32`. --- -## 1. Numeric scalars +## Quick recommendations (safe defaults) -In most places where a parameter is documented as `float` (or `float | int`), you can pass: +If you only read one section, read this: -- `float` (Python’s float is a C `double`) -- `int` (will be converted and used where appropriate) +- For **single values** (energy, LET, etc.): use Python `float` + (example: `150.0`, not `np.float32(150)`). +- For **arrays of values**: use NumPy arrays with `dtype=np.float64`. +- For **IDs** (material IDs, model IDs): use Python `int` or integer NumPy arrays (`np.int32` / `np.int64`). +- Avoid `numpy.float32` / `dtype=np.float32` for inputs to physics calculations unless you really know you can tolerate reduced precision. -Example: +--- -```python -import pyamtrack +## 1) What inputs are accepted? -pyamtrack.converters.beta_from_energy(150) # int OK -pyamtrack.converters.beta_from_energy(150.0) # float OK -``` +Most numeric functions accept these input forms: -Many wrapped functions internally convert scalar inputs to C++ `double` using `nb::cast(...)`. +### A. A single number +Use when you want one result. ---- +```python +import pyamtrack +pyamtrack.converters.beta_from_energy(150.0) +``` -## 2. Lists (vectorized “element-wise” calls) +### B. A list of numbers +Use when you want results for multiple values. -Many multi-argument functions are wrapped so that you can pass **Python lists** and get **vectorized** results. -The wrapper checks whether an argument is a Python `list` and then applies the computation element-by-element. +```python +import pyamtrack +energies = [50.0, 100.0, 150.0] +pyamtrack.stopping.electron_range(energies, material=1, model="tabata") +``` -Example: +### C. A NumPy array (`ndarray`) +Recommended for performance and clarity, especially for longer vectors. ```python +import numpy as np import pyamtrack -energies = [50.0, 100.0, 150.0] -r = pyamtrack.stopping.electron_range(energies, material=1, model="tabata") -# typically returns a NumPy array of float64 results +energies = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) +pyamtrack.stopping.electron_range(energies, material=1, model="tabata") ``` -### Broadcasting scalars against lists/arrays -If at least one argument is list/array, scalar arguments are broadcast to match the vector length. +**Note:** Some functions require NumPy input arrays to be **1‑dimensional** (a simple vector). --- -## 3. NumPy arrays / ndarrays +## 2) Continuous values vs IDs (different “kinds” of inputs) -### 3.1 Floating inputs (energies, continuous parameters, etc.) -For many numeric inputs (e.g. energies), you can pass a NumPy array. -Internally, wrappers cast ndarray inputs to a `double`-based view, i.e. values are consumed as **C++ `double`**. -Recommended: +In `pyamtrack`, it helps to think of inputs in two categories: -```python -import numpy as np -import pyamtrack +### A. Continuous physical quantities (use float64) +Examples: energies, ranges, stopping power values, etc. -energy = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) -r = pyamtrack.stopping.electron_range(energy, material=1, model="tabata") -``` +- Recommended dtype for arrays: **`np.float64`** +- Recommended type for scalars: Python **`float`** -### 3.2 Integer-coded inputs (IDs like `material` and `model`) -Some parameters are IDs (e.g. `material`, `model`) and accept: +### B. Integer identifiers (use integers) +Examples: `material` ID, sometimes model ID. -- scalar `int` -- Python list of ints / objects (depending on parameter) -- NumPy arrays **with integer dtype** +- Recommended dtype for arrays: **`np.int32`** or **`np.int64`** +- Recommended type for scalars: Python **`int`** -The library checks integer dtypes explicitly: `int8/16/32/64` and `uint8/16/32/64`. -Example (material IDs as an int array): +Example (material IDs as an integer array): ```python import numpy as np import pyamtrack -energies = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) -materials = np.asarray([1, 1, 1], dtype=np.int32) # integer dtype required for ID arrays +energies = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) +materials = np.asarray([1, 1, 1], dtype=np.int32) -r = pyamtrack.stopping.electron_range(energies, material=materials, model="tabata") +pyamtrack.stopping.electron_range(energies, material=materials, model="tabata") ``` -If you pass a NumPy array for an ID parameter but it is **not** an integer dtype, some wrappers will reject it. - --- -## 4. 0-D arrays and 1-D arrays +## 3) Why `numpy.float32` is not recommended -Some wrappers accept **0-D** or **1-D** arrays (for vectorized evaluation). -For certain vectorized wrappers, NumPy array inputs are required to be **1-D** (otherwise a `ValueError` is raised). +You will often see NumPy arrays created with `dtype=np.float32` to save memory. However, for `pyamtrack` this can be a bad default. -If you have higher-dimensional arrays, flatten them explicitly if that matches your intent: +### What happens +Even though the C/C++ code uses `double`, if you store inputs as `float32`: + +- the values are already rounded to about **7 significant digits**, +- and converting that float32 value to C/C++ `double` **cannot recover the lost precision**. + +So you can get slightly different results compared to float64 inputs. + +### What to do instead +Prefer `float64`: ```python -x = np.asarray(x) -x1d = x.reshape(-1) # or x.ravel() +import numpy as np + +# Not recommended for numerically sensitive inputs +x32 = np.asarray([0.1, 0.2, 0.3], dtype=np.float32) + +# Recommended +x64 = np.asarray([0.1, 0.2, 0.3], dtype=np.float64) + +# If you receive float32 from elsewhere, convert early: +x = np.asarray(x32, dtype=np.float64) ``` --- -## 5. IMPORTANT: avoid `numpy.float32` (precision loss) +## 4) NumPy dtype vs C type (quick reference) -Even though `numpy.float32` values are *convertible* to C++ `double`, using float32 can silently reduce precision. +This is a short summary based on the NumPy documentation section +“Relationship Between NumPy Data Types and C Data Types”. +([numpy.org](https://numpy.org/doc/stable/user/basics.types.html?utm_source=openai)) -### Why? -- `numpy.float32` stores only ~7 decimal digits of precision. -- When you pass a float32 into `pyamtrack`, it is converted and used as a C++ `double`. -- But conversion **cannot restore precision that was already lost** when the value was stored as float32. +| Recommended NumPy dtype | NumPy “C-like” name | Rough C type | Notes | +|---|---|---|---| +| `np.int32` | *(no single alias on every platform)* | usually `int32_t` | fixed width (portable) | +| `np.int64` | `np.longlong` | `long long` | fixed width (portable) | +| `np.float32` | `np.single` | `float` | ~7 significant digits | +| `np.float64` | `np.double` | `double` | ~15–16 significant digits | -So if you start with float32 inputs (scalar or array), you may see differences compared to float64/Python-float inputs—especially in calculations sensitive to small relative errors. +--- -### What to do instead -Prefer: -- Python `float` (scalar) -- `numpy.float64` (arrays) +## 5) “One value” vs “many values”: how results are returned + +Many functions behave like this: -Examples: +- If you pass a **single value**, you get a **single value** back (Python `float`). +- If you pass a **list** or **NumPy array**, you get a **vector of results** back (often a NumPy array). + +Example: ```python import numpy as np +import pyamtrack -# Bad (may lose precision before conversion to C double) -x_bad = np.float32(0.1) -arr_bad = np.asarray([0.1, 0.2, 0.3], dtype=np.float32) - -# Good -x_good = float(0.1) -arr_good = np.asarray([0.1, 0.2, 0.3], dtype=np.float64) +print(pyamtrack.converters.beta_from_energy(150.0)) # scalar -> scalar -# If you receive float32 data, upcast early: -arr = np.asarray(arr_bad, dtype=np.float64) +arr = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) +print(pyamtrack.converters.beta_from_energy(arr)) # array -> array ``` -### Rule of thumb -If a parameter represents a *continuous physical quantity* (energy, LET, stopping power inputs, etc.), -use **float64 / Python float** unless you have a strong reason not to. - --- -## 6. Notes on return types +## 6) Practical checklist for users -Many functions return: -- a Python `float` for scalar inputs -- a NumPy array (`float64`) for vectorized inputs -- and sometimes nested lists / arrays when using cartesian-product evaluation +Before running calculations, quickly check: -See the docstring of each function for the exact behavior (for example, `stopping.electron_range` describes returning either a scalar or a NumPy array depending on input). \ No newline at end of file +1. Are my **continuous values** stored as `float64`? + - `np.asarray(x, dtype=np.float64)` +2. Are my **ID-like inputs** (materials/models) integers? + - `np.asarray(ids, dtype=np.int32)` or plain `int` +3. Am I accidentally using `float32` because of upstream data loading? + - If yes: upcast early to `float64`. + +--- From b87a7971a925b901f9d7edecaaf37b8688601c32 Mon Sep 17 00:00:00 2001 From: PiotrPich2024 Date: Thu, 30 Apr 2026 23:25:45 +0200 Subject: [PATCH 3/5] Fix links and update type conversion documentation for clarity --- README.md | 2 +- docs/python/types.md | 176 ++++++++++++++----------------------------- 2 files changed, 59 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 38fd116..8ac43f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # libamtrack web documentation -This repository hosts source code of the libamtrack documentation available at [libamtrack.github.io](libamtrack.github.io) +This repository hosts source code of the libamtrack documentation available at [libamtrack.github.io](https://libamtrack.github.io) ## To install run: diff --git a/docs/python/types.md b/docs/python/types.md index 7295296..9a50abd 100644 --- a/docs/python/types.md +++ b/docs/python/types.md @@ -1,168 +1,108 @@ --- -sidebar_position: 5 +sidebar_position: 6 --- -# Input types (Python / NumPy) and numerical precision +# Type Conversion Tables for Ported Functions -`pyamtrack` is a Python interface to the C/C++ **libamtrack** library. Many functions accept either a **single number** or a **set of numbers** (a Python list or a NumPy array). Internally, most continuous physical quantities are computed using C/C++ **double precision** (`double`). - -This page explains what you can pass to `pyamtrack` functions and how to avoid the most common numerical pitfalls—especially when using `numpy.float32`. - ---- - -## Quick recommendations (safe defaults) - -If you only read one section, read this: - -- For **single values** (energy, LET, etc.): use Python `float` - (example: `150.0`, not `np.float32(150)`). -- For **arrays of values**: use NumPy arrays with `dtype=np.float64`. -- For **IDs** (material IDs, model IDs): use Python `int` or integer NumPy arrays (`np.int32` / `np.int64`). -- Avoid `numpy.float32` / `dtype=np.float32` for inputs to physics calculations unless you really know you can tolerate reduced precision. +This page shows input/output type conversions for all fully ported functions in pyamtrack. --- -## 1) What inputs are accepted? - -Most numeric functions accept these input forms: - -### A. A single number -Use when you want one result. - -```python -import pyamtrack -pyamtrack.converters.beta_from_energy(150.0) -``` - -### B. A list of numbers -Use when you want results for multiple values. +## `converters.beta_from_energy` -```python -import pyamtrack -energies = [50.0, 100.0, 150.0] -pyamtrack.stopping.electron_range(energies, material=1, model="tabata") -``` +Converts kinetic energy to relativistic beta. -### C. A NumPy array (`ndarray`) -Recommended for performance and clarity, especially for longer vectors. +| Python Input | → C Input | → C Output | → Python Output | +|---|---|---|---| +| `float` (energy in MeV) | `double` | `double` | `float` | +| `list` of floats | `double*` array | `double*` array | `np.ndarray` (float64) | +| `np.ndarray` (float64) | `double*` array | `double*` array | `np.ndarray` (float64) | +**Example:** ```python import numpy as np import pyamtrack -energies = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) -pyamtrack.stopping.electron_range(energies, material=1, model="tabata") -``` +# Scalar +beta = pyamtrack.converters.beta_from_energy(150.0) -**Note:** Some functions require NumPy input arrays to be **1‑dimensional** (a simple vector). +# Array +energies = np.linspace(10.0, 1000.0, 100, dtype=np.float64) +betas = pyamtrack.converters.beta_from_energy(energies) +``` --- -## 2) Continuous values vs IDs (different “kinds” of inputs) - -In `pyamtrack`, it helps to think of inputs in two categories: - -### A. Continuous physical quantities (use float64) -Examples: energies, ranges, stopping power values, etc. - -- Recommended dtype for arrays: **`np.float64`** -- Recommended type for scalars: Python **`float`** +## `converters.energy_from_beta` -### B. Integer identifiers (use integers) -Examples: `material` ID, sometimes model ID. +Converts relativistic beta to kinetic energy. -- Recommended dtype for arrays: **`np.int32`** or **`np.int64`** -- Recommended type for scalars: Python **`int`** - -Example (material IDs as an integer array): +| Python Input | → C Input | → C Output | → Python Output | +|---|---|---|---| +| `float` (beta value) | `double` | `double` | `float` | +| `list` of floats | `double*` array | `double*` array | `np.ndarray` (float64) | +| `np.ndarray` (float64) | `double*` array | `double*` array | `np.ndarray` (float64) | +**Example:** ```python import numpy as np import pyamtrack -energies = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) -materials = np.asarray([1, 1, 1], dtype=np.int32) +# Scalar +energy = pyamtrack.converters.energy_from_beta(0.5) -pyamtrack.stopping.electron_range(energies, material=materials, model="tabata") +# Array +betas = np.linspace(0.1, 0.9, 50, dtype=np.float64) +energies = pyamtrack.converters.energy_from_beta(betas) ``` --- -## 3) Why `numpy.float32` is not recommended - -You will often see NumPy arrays created with `dtype=np.float32` to save memory. However, for `pyamtrack` this can be a bad default. +## `stopping.electron_range` -### What happens -Even though the C/C++ code uses `double`, if you store inputs as `float32`: +Calculates electron range in materials using various models. -- the values are already rounded to about **7 significant digits**, -- and converting that float32 value to C/C++ `double` **cannot recover the lost precision**. - -So you can get slightly different results compared to float64 inputs. - -### What to do instead -Prefer `float64`: +| Python Input | → C Input | → C Output | → Python Output | +|---|---|---|---| +| `float` (energy in MeV) | `double` | `double` | `float` | +| `list` of floats | `double*` array | `double*` array | `np.ndarray` (float64) | +| `np.ndarray` (float64) | `double*` array | `double*` array | `np.ndarray` (float64) | +| `int` (material ID) | `int` | — | — | +| `str` (model name) | `char*` | — | — | +**Example:** ```python import numpy as np +import pyamtrack -# Not recommended for numerically sensitive inputs -x32 = np.asarray([0.1, 0.2, 0.3], dtype=np.float32) - -# Recommended -x64 = np.asarray([0.1, 0.2, 0.3], dtype=np.float64) +# Scalar +range_cm = pyamtrack.stopping.electron_range(150.0, material=1, model="tabata") -# If you receive float32 from elsewhere, convert early: -x = np.asarray(x32, dtype=np.float64) +# Array (recommended for plots) +energies = np.linspace(10.0, 1000.0, 500, dtype=np.float64) +ranges = pyamtrack.stopping.electron_range(energies, material=1, model="tabata") ``` --- -## 4) NumPy dtype vs C type (quick reference) +## `materials` module functions -This is a short summary based on the NumPy documentation section -“Relationship Between NumPy Data Types and C Data Types”. -([numpy.org](https://numpy.org/doc/stable/user/basics.types.html?utm_source=openai)) +Access and query material properties. -| Recommended NumPy dtype | NumPy “C-like” name | Rough C type | Notes | +| Python Input | → C Input | → C Output | → Python Output | |---|---|---|---| -| `np.int32` | *(no single alias on every platform)* | usually `int32_t` | fixed width (portable) | -| `np.int64` | `np.longlong` | `long long` | fixed width (portable) | -| `np.float32` | `np.single` | `float` | ~7 significant digits | -| `np.float64` | `np.double` | `double` | ~15–16 significant digits | - ---- - -## 5) “One value” vs “many values”: how results are returned - -Many functions behave like this: - -- If you pass a **single value**, you get a **single value** back (Python `float`). -- If you pass a **list** or **NumPy array**, you get a **vector of results** back (often a NumPy array). - -Example: +| `int` (material ID) | `int` | — | — | +| `str` (material name) | `char*` | — | — | +| `int` (property ID) | `int` | `double` / `int` | `float` / `int` | +| `list` of ints | `int*` array | `double*` / `int*` array | `np.ndarray` | +**Example:** ```python -import numpy as np import pyamtrack -print(pyamtrack.converters.beta_from_energy(150.0)) # scalar -> scalar +# Query material property +density = pyamtrack.materials.get_density(1) # material ID 1 -arr = np.asarray([50.0, 100.0, 150.0], dtype=np.float64) -print(pyamtrack.converters.beta_from_energy(arr)) # array -> array +# List available materials +materials = pyamtrack.materials.list_materials() ``` - ---- - -## 6) Practical checklist for users - -Before running calculations, quickly check: - -1. Are my **continuous values** stored as `float64`? - - `np.asarray(x, dtype=np.float64)` -2. Are my **ID-like inputs** (materials/models) integers? - - `np.asarray(ids, dtype=np.int32)` or plain `int` -3. Am I accidentally using `float32` because of upstream data loading? - - If yes: upcast early to `float64`. - ---- From 40e5595cfd6e89d9cfd34823935329fc9f236c12 Mon Sep 17 00:00:00 2001 From: PiotrPich2024 Date: Fri, 22 May 2026 12:42:55 +0200 Subject: [PATCH 4/5] Enhance types.md with detailed input type descriptions and examples --- docs/python/types.md | 238 ++++++++++++++++++++++++++++++------------- 1 file changed, 169 insertions(+), 69 deletions(-) diff --git a/docs/python/types.md b/docs/python/types.md index 9a50abd..5ad1493 100644 --- a/docs/python/types.md +++ b/docs/python/types.md @@ -1,108 +1,208 @@ +# pyamtrack — input types and vectorization behavior + +This document describes which Python/NumPy types are accepted by `pyamtrack` functions and how `pyamtrack` interprets inputs (scalars, lists, `numpy.ndarray`) and what types it returns. + +It specifically covers functions exported by modules (e.g. `pyamtrack.stopping`, `pyamtrack.converters`) that use the shared C++ wrappers in `src/wrapper/`. + --- -sidebar_position: 6 ---- -# Type Conversion Tables for Ported Functions +## 1. Glossary + +### Scalar +In `pyamtrack`, a scalar is a Python object of type: +- `float` +- `int` + +A scalar is treated as a single value (not as a sequence). -This page shows input/output type conversions for all fully ported functions in pyamtrack. +### Array-like +In `pyamtrack`, “array-like” means: +- `list` (Python list) +- `numpy.ndarray` + +**Note:** `tuple` is not treated as array-like and will usually raise a `TypeError`. --- -## `converters.beta_from_energy` +## 2. Input types + +### 2.1. Numeric Python values + +Most commonly accepted types are: +- `float` +- `int` -Converts kinetic energy to relativistic beta. +Many functions also work with mixed numeric elements inside lists (e.g. `[1, 2.0, 3]`), but this depends on the conversion path. -| Python Input | → C Input | → C Output | → Python Output | -|---|---|---|---| -| `float` (energy in MeV) | `double` | `double` | `float` | -| `list` of floats | `double*` array | `double*` array | `np.ndarray` (float64) | -| `np.ndarray` (float64) | `double*` array | `double*` array | `np.ndarray` (float64) | +If an argument is not `float`, `int`, `list`, or `numpy.ndarray`, a type error will be raised. **Example:** -```python -import numpy as np -import pyamtrack +```py +pyamtrack.stopping.electron_range((50.0,)) +# TypeError: Input must be a float, int, list, or 0-D/1-D NumPy array. +``` + +--- -# Scalar -beta = pyamtrack.converters.beta_from_energy(150.0) +### 2.2. Python lists (`list`) -# Array -energies = np.linspace(10.0, 1000.0, 100, dtype=np.float64) -betas = pyamtrack.converters.beta_from_energy(energies) +Many functions accept a list of values and return a vectorized result. + +Example: +```py +pyamtrack.stopping.electron_range([50.0, 100.0, 150.0]) +# -> returns numpy.ndarray ``` +#### List lengths in multi-argument functions +For functions that take multiple arguments (e.g. `electron_range(energy, material, model)`), if you pass lists in more than one argument, their lengths must match in “element-wise” mode. + +If they do not match: +- `ValueError: Incompatible lists/arrays size` + --- -## `converters.energy_from_beta` +### 2.3. NumPy arrays (`numpy.ndarray`) -Converts relativistic beta to kinetic energy. +`pyamtrack` accepts `numpy.ndarray`, but the wrappers have important constraints depending on the execution mode. -| Python Input | → C Input | → C Output | → Python Output | -|---|---|---|---| -| `float` (beta value) | `double` | `double` | `float` | -| `list` of floats | `double*` array | `double*` array | `np.ndarray` (float64) | -| `np.ndarray` (float64) | `double*` array | `double*` array | `np.ndarray` (float64) | +#### 2.3.1. Element-wise mode (“zip-style” vectorization) +In element-wise mode (`wrap_multiargument_function`), NumPy arrays must be: +- **one-dimensional (1-D)** -**Example:** -```python -import numpy as np -import pyamtrack +If `ndim != 1`: +- `ValueError: Input NumPy array must be 1-D.` -# Scalar -energy = pyamtrack.converters.energy_from_beta(0.5) +Dtype: +- values are typically cast to `double` (float64) in the wrapper +- if the dtype cannot be cast: + - `TypeError: 1-D NumPy array dtype cannot be cast to double or input is not suitable.` -# Array -betas = np.linspace(0.1, 0.9, 50, dtype=np.float64) -energies = pyamtrack.converters.energy_from_beta(betas) +Example: +```py +energy = np.array([50.0, 100.0], dtype=np.float64) +pyamtrack.stopping.electron_range(energy) +# -> numpy.ndarray(shape=(2,)) +``` + +#### 2.3.2. Cartesian product mode (combinatorics) +In cartesian product mode (`wrap_cartesian_product_function`), NumPy arrays: +- may be multi-dimensional (e.g. `(2,2)`, `(10,10,10)`), +- but must be **C-contiguous** (row-major contiguous in memory). + +If an array is not C-contiguous: +- `ValueError: NDArray must be C-contiguous. Use numpy.ascontiguousarray(your_array) before passing it.` + +In this mode, input ndarrays are flattened to 1-D for generating combinations, while the original shape is recorded for shaping the output (depending on the wrapper). + +Example: +```py +energy = np.array([[50.0, 100.0], + [150.0, 200.0]], order="C") +materials = np.array([1, 2, 3], dtype=np.int64) +models = np.array([7, 8], dtype=np.int64) + +pyamtrack.stopping.electron_range(energy, materials, models, cartesian_product=True) +# -> numpy.ndarray with a shape derived from input shapes ``` --- -## `stopping.electron_range` +## 3. Return types (outputs) -Calculates electron range in materials using various models. +### 3.1. Scalar in → scalar out +If all arguments are scalars (`float`/`int`), the result is a scalar (Python `float`). -| Python Input | → C Input | → C Output | → Python Output | -|---|---|---|---| -| `float` (energy in MeV) | `double` | `double` | `float` | -| `list` of floats | `double*` array | `double*` array | `np.ndarray` (float64) | -| `np.ndarray` (float64) | `double*` array | `double*` array | `np.ndarray` (float64) | -| `int` (material ID) | `int` | — | — | -| `str` (model name) | `char*` | — | — | +Example: +```py +pyamtrack.stopping.electron_range(100.0, 1, 7) +# -> float +``` -**Example:** -```python -import numpy as np -import pyamtrack +### 3.2. Array-like in → numpy.ndarray out +If at least one argument is a list or `numpy.ndarray` in element-wise mode, the result is usually a 1‑D `numpy.ndarray` with length matching the list/array length. + +Example: +```py +pyamtrack.stopping.electron_range([50.0, 100.0], 1, 7) +# -> np.ndarray(shape=(2,)) +``` + +### 3.3. Cartesian product → numpy.ndarray (multi-dimensional) +If `cartesian_product=True`, the result is a `numpy.ndarray` whose size corresponds to the number of argument combinations. + +--- -# Scalar -range_cm = pyamtrack.stopping.electron_range(150.0, material=1, model="tabata") +## 4. Broadcasting (scalar expansion) -# Array (recommended for plots) -energies = np.linspace(10.0, 1000.0, 500, dtype=np.float64) -ranges = pyamtrack.stopping.electron_range(energies, material=1, model="tabata") +In element-wise mode, if you pass a mix of: +- one argument as a vector (list/ndarray) of length `N`, +- another argument as a scalar, + +the scalar will be **expanded** to length `N` (broadcast to 1‑D) and the computation is done element-wise. + +Example: +```py +energy = [50.0, 100.0, 150.0] +pyamtrack.stopping.electron_range(energy, material=1, model=7) +# model and material are scalars -> treated like [1, 1, 1] and [7, 7, 7] ``` --- -## `materials` module functions +## 5. Errors and exceptions -Access and query material properties. +Below are typical exceptions raised by the wrappers: -| Python Input | → C Input | → C Output | → Python Output | -|---|---|---|---| -| `int` (material ID) | `int` | — | — | -| `str` (material name) | `char*` | — | — | -| `int` (property ID) | `int` | `double` / `int` | `float` / `int` | -| `list` of ints | `int*` array | `double*` / `int*` array | `np.ndarray` | +### 5.1. Unsupported argument type +**TypeError**: +- `Input must be a float, int, list, or 0-D/1-D NumPy array.` +- `Input must be a float, int, list, or NumPy array.` (cartesian product mode) -**Example:** -```python -import pyamtrack +Typical causes: +- passing `tuple`, `dict`, user-defined objects, `None`, etc. -# Query material property -density = pyamtrack.materials.get_density(1) # material ID 1 +### 5.2. Incompatible list/array lengths in element-wise mode +**ValueError**: +- `Incompatible lists/arrays size` -# List available materials -materials = pyamtrack.materials.list_materials() -``` +### 5.3. Wrong ndarray dimensionality in element-wise mode +**ValueError**: +- `Input NumPy array must be 1-D.` + +### 5.4. Non C-contiguous ndarray in cartesian product mode +**ValueError**: +- `NDArray must be C-contiguous. Use numpy.ascontiguousarray(your_array) before passing it.` + +### 5.5. Dtype cannot be cast to double +**TypeError**: +- `1-D NumPy array dtype cannot be cast to double or input is not suitable.` + +--- + +## 6. Practical recommendations + +1. If you have a `tuple`, convert it to a list: + ```py + x = (1.0, 2.0) + x = list(x) + ``` + +2. If you have multi-dimensional NumPy data and use `cartesian_product=True`, ensure it is C-contiguous: + ```py + x = np.ascontiguousarray(x) + ``` + +3. If a function in element-wise mode complains about `1-D`, use `.ravel()` or `.reshape(-1)`: + ```py + x = np.asarray(x).ravel() + ``` + +--- + +## 7. “Element-wise” vs “Cartesian product” — quick comparison + +| Mode | Purpose | How it combines arguments | Typical output | +|------|---------|----------------------------|----------------| +| element-wise | zip-style vectorization | (a[i], b[i], c[i]) | 1-D `np.ndarray` | +| cartesian product | combinations | all combinations of arguments | N-D `np.ndarray` | \ No newline at end of file From c92f1f6faa342c07c89cae05dee1d0c36faf6837 Mon Sep 17 00:00:00 2001 From: PiotrPich2024 Date: Fri, 22 May 2026 14:19:36 +0200 Subject: [PATCH 5/5] Add documentation on floating-point precision and dtype casting for NumPy arrays --- docs/python/types.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/python/types.md b/docs/python/types.md index 5ad1493..1303235 100644 --- a/docs/python/types.md +++ b/docs/python/types.md @@ -66,6 +66,22 @@ If they do not match: `pyamtrack` accepts `numpy.ndarray`, but the wrappers have important constraints depending on the execution mode. +#### Floating-point precision (dtype) and casting + +Most `pyamtrack` numerical kernels are implemented in C/C++ and operate on **double precision** (`float64`) values. As a result, `numpy.ndarray` inputs are typically **cast to `float64`** (C++ `double`) by the binding/wrapper layer before computation. + +This has a few important consequences: + +- Passing `float32`, `float16`, or other floating dtypes usually **does not preserve the original precision** during computation; values are converted to `float64` first. +- The conversion may require an **implicit copy** of the input array, which can increase memory use and reduce performance for large arrays. +- If you need strict control over dtype/precision for performance or memory reasons, be aware that the current `pyamtrack` API is effectively **`float64`-centric** for floating-point computations. + +**Recommendation:** when using NumPy arrays, prefer explicit `float64` inputs to make the conversion behavior obvious: + +```python +x = np.asarray(x, dtype=np.float64) +``` + #### 2.3.1. Element-wise mode (“zip-style” vectorization) In element-wise mode (`wrap_multiargument_function`), NumPy arrays must be: - **one-dimensional (1-D)**