Metadata-Version: 2.4
Name: fit_python
Version: 1.0.0
Summary: Forward-in-Time spatial object identification and temporal tracking
Author: Gregor Skok
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C++
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23
Requires-Dist: Pillow
Dynamic: license-file

# FiT Python: Object Analysis and Tracking

FiT (Forward-in-Time) identifies and tracks objects in a sequence of 3D or 2D
fields. The method can be applied to different variables, such as precipitation
or vertical velocity. Its results describe object location, size, lifespan,
trajectories, and the spatial overlaps linking consecutive timesteps.

This Python library provides a NumPy interface to the C++ implementation.
Spatial identification uses cascading threshold categories, and temporal
tracking links the identified objects through time. Tracking settings and file
operations are controlled from Python.

The package is distributed and imported as `fit_python`.

GitHub repository: [skokg/FiT](https://github.com/skokg/FiT).

## Installation

Python 3.10 or newer is required. Install the package with:

```sh
pip install fit_python
```

To install from source:

```sh
pip install --no-binary=fit_python fit_python
```

Compiling from source requires a C++ compiler: GCC or Clang on Linux, MSVC Build
Tools on Windows, or Xcode Command Line Tools on macOS.

The package can also be installed from conda-forge:

```sh
conda install -c conda-forge fit_python
```

Alternatively, download the source from the
[GitHub repository](https://github.com/skokg/FiT) and run `make` in the project's
root directory on Linux. This requires GNU Make and a C++ compiler.
Running `make` creates `libfit_python.so` in the `src` folder. Copy
`fit_python.py` and `libfit_python.so` from `src` into the folder containing your
Python script. NumPy and Pillow must also be installed.

## Usage

Import the library with:

```python
import fit_python as fit
```

Inputs are integer threshold categories, not unprocessed physical values.
Apply physical thresholds and any smoothing before calling FiT:

* `0`: background.
* `-1`: missing or masked cells.
* `1..max_threshold_level`: increasing cascading threshold categories.

For a finite continuous NumPy field, generate categories in one line:

```python
# Apply increasing physical thresholds; each cell records how many it reaches.
# The resulting int64 categories are 0, 1, 2 or 3.
field_thresholded = np.digitize(field_original_continuous, bins=[1, 5, 10]).astype(np.int64)
```

This gives 0 below 1, 1 from 1 to below 5, 2 from 5 to below 10, and 3 at or
above 10. Set missing cells to `-1` and use the same thresholds for all timesteps.

A field has shape `(y, x)` or `(z, y, x)`. The default
`max_threshold_level=10` is an upper bound on input categories. Identification
uses face connectivity: four neighbors in 2D and six in 3D.

A spatial object is identified at one timestep. A temporal object links spatial
objects through time and may contain multiple separate pieces at a timestep.

The main functions and methods are:

* `fit.Tracker()`: create a tracker for a sequence of fields.
* `tracker.step()`: identify and track one timestep.
* `tracker.step_batch()`: process several timesteps, optionally in parallel.
* `tracker.spatial_objects()`, `tracker.temporal_objects()` and
  `tracker.overlaps()`: retrieve object properties and overlap history.
* `fit.save_png()` and `fit.save_table()`: save label images and result tables.

Use one persistent tracker to process a sequence:

```python
import numpy as np
import fit_python as fit

# Create a lower-threshold region with a higher-threshold core inside it.
# Allocate a 5-row, 8-column field; zeros represent background.
first_field = np.zeros((5, 8), dtype=np.int64)
# Fill the outer region with category 1.
first_field[1:4, 3:7] = 1
# Replace its inner cells with category 2, representing a higher threshold.
first_field[2, 4:6] = 2
# Add a separate two-cell event that disappears after this timestep.
first_field[0:2, 0] = 1

# Shift the main region right and shrink it from 12 cells to 9 cells.
second_field = np.zeros_like(first_field)
second_field[1:4, 4:7] = 1
second_field[2, 5] = 2
# Add a different two-cell event, with no overlap with either previous event.
second_field[3:5, 1] = 1

# Input categories (rows are y; columns are x):
# first_field (timestep 0)    second_field (timestep 1)
# 1 0 0 0 0 0 0 0          0 0 0 0 0 0 0 0
# 1 0 0 1 1 1 1 0          0 0 0 0 1 1 1 0
# 0 0 0 1 2 2 1 0          0 0 0 0 1 2 1 0
# 0 0 0 1 1 1 1 0          0 1 0 0 1 1 1 0
# 0 0 0 0 0 0 0 0          0 1 0 0 0 0 0 0

# Identify spatial objects and track them between consecutive fields.
with fit.Tracker(max_threshold_level=10, periodic_x=False, merging_distance=-1) as tracker:
    # Process the first field.
    result = tracker.step(first_field)

    # Copy both label fields before the next step reuses their storage.
    first_spatial_labels = result.spatial_labels.copy()
    first_temporal_labels = result.temporal_labels.copy()

    # Process the second field.
    result = tracker.step(second_field)

    # Copy both label fields before the next step reuses their storage.
    second_spatial_labels = result.spatial_labels.copy()
    second_temporal_labels = result.temporal_labels.copy()

    # Collect spatial object properties from both timesteps.
    spatial_history = tracker.spatial_objects()
    # Collect temporal properties, including age and number of spatial pieces.
    temporal_history = tracker.temporal_objects()
    # Collect links and shared-cell counts between consecutive spatial objects.
    overlap_history = tracker.overlaps()

print(first_spatial_labels, second_spatial_labels)
# Spatial labels (shown side by side here for readability):
# First timestep            Second timestep
# 2 0 0 0 0 0 0 0          0 0 0 0 0 0 0 0
# 2 0 0 1 1 1 1 0          0 0 0 0 1 1 1 0
# 0 0 0 1 1 1 1 0          0 0 0 0 1 1 1 0
# 0 0 0 1 1 1 1 0          0 2 0 0 1 1 1 0
# 0 0 0 0 0 0 0 0          0 2 0 0 0 0 0 0

print(first_temporal_labels, second_temporal_labels)
# Temporal labels (shown side by side here for readability):
# First timestep            Second timestep
# 2 0 0 0 0 0 0 0          0 0 0 0 0 0 0 0
# 2 0 0 1 1 1 1 0          0 0 0 0 1 1 1 0
# 0 0 0 1 1 1 1 0          0 0 0 0 1 1 1 0
# 0 0 0 1 1 1 1 0          0 3 0 0 1 1 1 0
# 0 0 0 0 0 0 0 0          0 3 0 0 0 0 0 0
# The main event keeps temporal ID 1; the new small event gets temporal ID 3.

# Display the spatial, temporal and overlap history for both timesteps.
print(spatial_history)
# spatial_history (values shown as a table for readability):
# timestep spatial_id size xmin xmax ymin ymax zmin zmax centerx centery centerz temporal_id
#        0          1   12    3    6    1    3    0    0     4.5     2.0     0.0           1
#        0          2    2    0    0    0    1    0    0     0.0     0.5     0.0           2
#        1          1    9    4    6    1    3    0    0     5.0     2.0     0.0           1
#        1          2    2    1    1    3    4    0    0     1.0     3.5     0.0           3

print(temporal_history)
# temporal_history:
# timestep temporal_id size pieces lifestep xmin xmax ymin ymax zmin zmax centerx centery centerz
#        0           1   12      1        0    3    6    1    3    0    0     4.5     2.0     0.0
#        0           2    2      1        0    0    0    0    1    0    0     0.0     0.5     0.0
#        1           1    9      1        1    4    6    1    3    0    0     5.0     2.0     0.0
#        1           3    2      1        0    1    1    3    4    0    0     1.0     3.5     0.0
# Temporal object 1 continues with lifestep 1; object 3 is newly born.
# In this 2D example, all z bounds and centerz values are 0.

print(overlap_history)
# overlap_history:
# timestep spatial_id previous_timestep_spatial_id overlap_size temporal_id
#        1          1                            1            9           1
# The second main event shares 9 cells with the first main event.
# The first timestep and the new small event have no previous overlap rows.
```

The category-2 core represents a higher threshold inside the surrounding
category-1 region. FiT identifies the core first, then grows it into the
lower-threshold region. Input values 1 and 2 are threshold categories;
the output label fields contain spatial or temporal object IDs.

All fields in a tracker must have the same shape. Spatial IDs start at 1 and
restart each timestep; temporal IDs start at 1 and continue through tracking.
Background and missing values remain 0 and -1. Each current spatial object
inherits the temporal ID of its largest-overlap previous spatial object;
equal overlaps select the smaller previous spatial ID. Temporal tracking is
always performed.

### Parallel batches

Use `step_batch()` to identify different timesteps spatially in parallel while
completing temporal tracking in order.

Usage is demonstrated in the parallel examples:
`examples/example_2D_parallel.py` and `examples/example_3D_parallel.py`.

## Examples

The source distribution and GitHub repository include four scripts and their
sample NetCDF data in `examples/`. Examples and data are not installed into
`site-packages` by a wheel. Download and extract the source to use them.

Install the NetCDF dependency needed by the examples:

```sh
pip install netCDF4
```

Then run the examples from the downloaded source root:

```sh
python examples/example_2D.py
python examples/example_2D_parallel.py
python examples/example_3D.py
python examples/example_3D_parallel.py
```

| Script | Input | Output directory |
| --- | --- | --- |
| `example_2D.py` | 16 fields, shape `(100, 379)` | `examples/output_example_2D/` |
| `example_2D_parallel.py` | Same 2D fields | `examples/output_example_2D_parallel/` |
| `example_3D.py` | 11 fields, shape `(51, 339, 455)` | `examples/output_example_3D/` |
| `example_3D_parallel.py` | Same 3D fields | `examples/output_example_3D_parallel/` |

Each example writes spatial and temporal NetCDF fields, flattened PNG images,
and three CSV tables: `spatial_objects.csv`, `temporal_objects.csv`, and
`overlaps.csv`.

## Function descriptions

The following describes the main functions and methods used above. Label
fields are NumPy arrays; object and overlap tables are one-dimensional
structured NumPy arrays with named columns.

### `fit.Tracker(max_threshold_level=10, periodic_x=False, merging_distance=-1)`

**Inputs:** `max_threshold_level` is a positive integer giving the largest
allowed input category, not a list of physical thresholds. `periodic_x` is a
Boolean enabling connectivity across the x boundaries. `merging_distance`
limits merging spatial objects into a temporal object with multiple pieces
by their horizontal Euclidean distance from the combined temporal object's
center, measured in grid cells along x and y (ignoring z); it must be finite
and at least `-1`, with negative values disabling the limit and nonnegative
values, including zero, enabling it.

**Output:** A new `Tracker` with independent state and no processed timesteps.

**Functionality:** Stores the settings and maintains spatial object properties,
temporal identities and overlaps as fields are processed. The first processed
field fixes the shape for the entire sequence. Use a `with` block to close the
tracker automatically, or call `tracker.close()` when finished.

### `tracker.step(field)`

**Input:** A category field shaped `(y, x)` or `(z, y, x)`, with nonempty spatial
dimensions and the same shape as earlier fields in this tracker. Values must
be `-1`, `0`, or integers up to `max_threshold_level`. Masked cells become `-1`;
integer-valued finite floating-point input is accepted and converted to
native `int64`. Fractional, nonfinite and out-of-range values are rejected.

**Output:** A `FrameResult` with the following attributes:

| Attribute | Contents |
| --- | --- |
| `FrameResult.timestep` | Current timestep, starting at 0. |
| `FrameResult.spatial_labels` | Grid of spatial object IDs, with the same shape as the input field. |
| `FrameResult.temporal_labels` | Grid of temporal object IDs, with the same shape as the input field. |
| `FrameResult.spatial_objects` | Structured NumPy table of spatial object properties for this timestep. |
| `FrameResult.temporal_objects` | Structured NumPy table of temporal object properties for this timestep. |
| `FrameResult.overlaps` | Structured NumPy table of spatial overlaps with the preceding timestep; empty for the first timestep. |

The label fields are read-only views of reusable tracker-owned arrays. Copy
them before the next step if they must be retained. The three tables are
independent copies. Closing the tracker does not invalidate returned arrays.

Positive labels match `spatial_id` or `temporal_id` in the corresponding table
for that timestep, allowing you to select an object's cells and calculate
additional properties.

**Functionality:** Identifies objects from highest to lowest category, then
links them to the preceding field through spatial overlap. Each current
spatial object inherits the temporal ID of its largest-overlap previous object;
ties select the smaller previous spatial ID. Objects without overlap receive
new temporal IDs. Timesteps start at 0 and advance by one per field. Label
arrays are reused: copy earlier labels before processing another field.

### `tracker.step_batch(batch_fields, number_of_workers=1)`

**Inputs:** `batch_fields` is a NumPy array shaped `(batch, y, x)` or
`(batch, z, y, x)`. Each field follows the same category and shape requirements
as `step()`. `number_of_workers` is a positive integer; its default is 1.

**Output:** A list of `FrameResult` objects, one per input field, in input order.
For `results = tracker.step_batch(batch_fields)`, `results[i]` describes
`batch_fields[i]` and has the same attributes as the result of `step()`:

| Attribute | Contents |
| --- | --- |
| `FrameResult.timestep` | Timestep within the tracker's complete sequence; continues from any previously processed fields. |
| `FrameResult.spatial_labels` | Spatial label field shaped `(y, x)` or `(z, y, x)`, matching one input field rather than the whole batch. |
| `FrameResult.temporal_labels` | Temporal label field with the same shape as the spatial label field. |
| `FrameResult.spatial_objects` | Structured NumPy table of spatial object properties for this timestep. |
| `FrameResult.temporal_objects` | Structured NumPy table of temporal object properties for this timestep. |
| `FrameResult.overlaps` | Structured NumPy table of spatial overlaps with the preceding timestep, including across batch boundaries; empty only when there are no overlaps. |

Each frame's label fields occupy separate slices of reusable batch buffers,
so all frames in the returned batch remain available together. Later processing
calls may overwrite those buffers; copy labels before reusing the tracker if
they must be retained. The three tables in each result are independent copies,
and closing the tracker does not invalidate returned arrays. An empty batch
returns an empty list.

**Functionality:** Identifies timesteps spatially using the requested number of
workers, then completes temporal tracking in sequence. Processing continues
from the tracker's previous timestep and history. More than one worker enables
parallel spatial identification. Returned labels use reusable batch buffers;
copy any labels that must survive later calls. The parallel example scripts
demonstrate usage.

### `tracker.spatial_objects(timestep=None)`

**Input:** Omit `timestep`, or pass `None`, to select all processed timesteps.
Pass a nonnegative integer to select one already processed timestep.

**Output:** An independent structured NumPy array with one row per spatial
object per selected timestep. Columns include `timestep`, `spatial_id`,
`temporal_id`, `size`, bounds (`xmin`, `xmax`, `ymin`, `ymax`, `zmin`, `zmax`)
and centers (`centerx`, `centery`, `centerz`).

**Functionality:** Copies spatial object properties from the accumulated
history. Spatial IDs restart at 1 each timestep, so use both `timestep` and
`spatial_id` to identify a particular spatial object. Sizes count grid cells;
bounds are inclusive and coordinates are zero-based grid indices.

### `tracker.temporal_objects(timestep=None)`

**Input:** `None` selects all processed timesteps; a nonnegative integer selects
one already processed timestep.

**Output:** An independent structured NumPy array with one row per temporal
object per selected timestep. Columns include `timestep`, `temporal_id`, `size`,
`pieces`, `lifestep`, bounds and centers. There is no `spatial_id` column.

**Functionality:** Combines the spatial pieces sharing a temporal ID within each
timestep. `pieces` counts those components and `size` counts their combined
cells. `lifestep` is 0 at birth and increases on subsequent timesteps. Rows with
the same temporal ID describe its trajectory; the table is not reduced to one
row per complete lifetime.

### `tracker.overlaps(timestep=None)`

**Input:** `None` selects the entire overlap history; a nonnegative integer
selects one already processed current timestep.

**Output:** An independent structured NumPy array with columns `timestep`,
`spatial_id`, `previous_timestep_spatial_id`, `overlap_size` and `temporal_id`.
There are no overlap rows for the first timestep.

**Functionality:** Reports shared-cell counts between current and previous
spatial objects. `spatial_id` and `temporal_id` refer to the current object;
`previous_timestep_spatial_id` refers to the preceding timestep. The table
includes all overlapping pairs, allowing splits and mergers to be examined.

### `tracker.close()`

**Inputs:** None.

**Output:** `None`.

**Functionality:** Releases the tracker's native state, history and spatial
workers. Repeated calls are safe. Retrieve history tables before closing;
already returned arrays and table copies remain valid. Further processing or
history queries require a new tracker. Exiting a `with` block calls this method
automatically.

### `fit.render_rgb(labels, *, flip_y=True)`

**Inputs:** `labels` is a 2D or 3D integer label field with background `0`,
missing value `-1` and positive object IDs. `flip_y` is a keyword-only Boolean;
the default `True` makes increasing grid y point upward in the image.

**Output:** A NumPy `uint8` array shaped `(y, x, 3)` containing RGB pixels.

**Functionality:** Colors cells by object ID, with white background and missing
cells. For 3D input, the object at the lowest occupied z position in each column
sets the color; darker shading indicates more positive cells in that column.
No file is written. Colors can differ between C++ toolchains.

### `fit.save_png(path, labels, *, flip_y=True)`

**Inputs:** `path` is the destination filename or path object. `labels` and the
keyword-only `flip_y` follow the requirements of `render_rgb()`.

**Output:** A `pathlib.Path` pointing to the written PNG file.

**Functionality:** Calls `render_rgb()` and uses Pillow to encode the pixels as
PNG. Creates missing parent directories and overwrites an existing destination
file. It saves an image of the labels, rather than a numerical field for later
tracking.

### `fit.save_table(path, table)`

**Inputs:** `path` is the destination filename or path object. `table` is a
one-dimensional structured NumPy array, such as a spatial object, temporal
object or overlap table returned by the tracker.

**Output:** A `pathlib.Path` pointing to the written CSV file.

**Functionality:** Writes column names in the first row and one record per
subsequent row, using UTF-8 encoding. Creates missing parent directories and
overwrites an existing destination file. An empty table still produces the
column header.

## Author and license

Gregor Skok, Faculty of Mathematics and Physics, University of Ljubljana,
Slovenia.

Email: [Gregor.Skok@fmf.uni-lj.si](mailto:Gregor.Skok@fmf.uni-lj.si).
Please contact the author if you have questions or trouble using the package.

Licensed under the MIT License. See LICENSE.

## References

The papers below describe cascading thresholds and temporal matching; please
cite them when publishing work using FiT:

* Skok, G., Bacmeister, J. T., Tribbia, J. (2013). Analysis of tropical cyclone
  precipitation using an object-based algorithm. Journal of Climate, 26,
  2563-2579.
* Skok, G., Tribbia, J., Rakovec, J. (2010). Object-based analysis and verification
  of WRF model precipitation in the low- and Midlatitude Pacific Ocean. Monthly
  Weather Review, 138, 4561-4575.
* Skok, G., Tribbia, J., Rakovec, J., Brown, B. (2009). Object-based analysis of
  satellite-derived precipitation systems over the low- and midlatitude Pacific
  Ocean. Monthly Weather Review, 137, 3196-3218.
