From ffd5d1689e2f9688d9cd41c398c501a38c70f9e0 Mon Sep 17 00:00:00 2001 From: PiotrPich2024 Date: Fri, 13 Mar 2026 18:58:04 +0100 Subject: [PATCH 1/3] 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/3] 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/3] 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`. - ----