Metadata-Version: 2.4
Name: flashpdm
Version: 0.1.12
Summary: Python client for the Flashmind PDM forecasting API
Project-URL: Homepage, https://www.flashmind.com
Project-URL: Documentation, https://www.flashmind.com/docs/python
Project-URL: Support, https://www.flashmind.com/contact
Author: Flashmind Labs
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.28
Provides-Extra: arrays
Requires-Dist: numpy>=1.26; (python_version < '3.13') and extra == 'arrays'
Requires-Dist: numpy>=2.1; (python_version >= '3.13') and extra == 'arrays'
Requires-Dist: orjson<4,>=3.11; extra == 'arrays'
Provides-Extra: batch
Requires-Dist: joblib<2,>=1.4; extra == 'batch'
Requires-Dist: numpy>=1.26; (python_version < '3.13') and extra == 'batch'
Requires-Dist: numpy>=2.1; (python_version >= '3.13') and extra == 'batch'
Requires-Dist: orjson<4,>=3.11; extra == 'batch'
Provides-Extra: pandas
Requires-Dist: pandas<4,>=2.2; extra == 'pandas'
Description-Content-Type: text/markdown

# flashpdm

The Python client for the [Flashmind forecasting API](https://www.flashmind.com/docs/api).
Send time-series observations and receive a labeled `ForecastResult` containing
quantiles, variable types and request metadata. Conversion to pandas, NumPy,
dictionaries or CSV happens locally, without another forecast request.

[SDK usage guide](https://www.flashmind.com/docs/python) ·
[HTTP API reference](https://www.flashmind.com/docs/api) ·
[Account limits](https://www.flashmind.com/docs/limits) ·
[Support](https://www.flashmind.com/contact)

| Model | Parameters | Availability |
| --- | --- | --- |
| `pdm-femto` | 6.18M | Available |
| `pdm-pico` | 32.2M | Available |
| `pdm-nano` | 119M | Available; default |

All three models return the same labeled forecast structure. Capabilities:
`GET /v1/models/pdm-femto`, `GET /v1/models/pdm-pico` and `GET /v1/models/pdm-nano`.

## Install

Requires Python 3.10 or newer. The base client depends only on `httpx`:

```bash
python -m pip install flashpdm
```

Install optional integrations only when needed:

```bash
python -m pip install 'flashpdm[arrays]'         # NumPy input/output and faster JSON encoding
python -m pip install 'flashpdm[pandas]'         # pandas input/output and accessor
python -m pip install 'flashpdm[arrays,pandas]'  # Both
```

For reproducible environments, pin the version, for example
`python -m pip install 'flashpdm[arrays,pandas]==0.1.12'`.

## Your first forecast

This complete example works without an API key, within anonymous limits:

```python
from flashpdm import Client

inputs = {
    "store-42": {"targets": {"sales": [120, 132, 125, 141, 148, 137]}}
}
with Client() as client:
    forecast = client.forecast(inputs=inputs, horizon=3)

sales = forecast["store-42"]["sales"]
print(sales.median)
print(sales.interval(0.8))  # p10 and p90
print(forecast.request_id)
```

`store-42` names an independent input and `sales` names a target. Names match
inputs to outputs; they are not model features. `horizon=3` requests the next
three steps at the same cadence as the observations. Results remain usable after
the client closes. Reuse one client across requests to reuse HTTP connections.

All 21 native quantiles are returned by default: 0.01, 0.05, 0.10, …, 0.95, 0.99.
Select fewer with `quantile_levels=[0.1, 0.5, 0.9]`. Levels must increase strictly
within [0.01, 0.99]; the API interpolates non-native levels. `median`, `quantile()`
and `interval()` require the selected levels to be present; they never silently
substitute another level. The median is not the mean. Quantiles describe marginal
uncertainty at each step, not sampled trajectories or guaranteed coverage.

## Authentication and account limits

[Create an API key](https://www.flashmind.com/sign-in?returnTo=/dashboard/api-keys)
and set it in the environment, outside source control:

```bash
export FLASHMIND_API_KEY='your-api-key'
```

`Client()` and `AsyncClient()` read this environment variable automatically.
Alternatively pass `Client(api_key=...)` using your application's secret store.
Without either, requests use anonymous access. Invalid supplied credentials fail;
the client never silently falls back to anonymous traffic.

Multiple targets, covariates and larger requests require sufficient account
limits. The examples below demonstrate supported request structures, but a free
or anonymous account may not permit every example's channel count or horizon.
See [usage and limits](https://www.flashmind.com/docs/limits).

## Input structure and missing values

Each input contains `targets` and optionally `past_covariates`,
`future_covariates`, and `schema`. All are maps keyed by variable name.

- Different inputs are independent; multiple targets within one input are forecast jointly.
- Observations run oldest to newest at a regular cadence. Preserve missing steps using `None`; do not remove them and shift the time grid.
- Each history contains at least two observations. Histories within one input have equal lengths; different inputs may have different lengths.
- Every retained target history needs at least one observed value.
- Future covariates contain exactly `horizon` values known at the forecast cutoff. They currently support one target per input.
- Matching past/future keys identify one variable. A future-only covariate has missing history. Targets and covariates must have distinct names.

A complete example with two independent inputs:

```python
from flashpdm import Client

inputs = {
    "store-42": {"targets": {"sales": [120, 132, None, 141]}},
    "store-77": {"targets": {"sales": [90, 95, 93, 101]}},
}
with Client() as client:
    forecast = client.forecast(inputs=inputs, horizon=3)
print(forecast["store-77"]["sales"].median)
```

## Covariates, types and bounds

This example forecasts nonnegative integer sales using known promotions and
weather. It requires an account allowing five encoded channels:

```python
from flashpdm import Client

inputs = {"store-42": {
    "targets": {"sales": [120, 132, 125, 141]},
    "past_covariates": {
        "promotion": [False, True, False, True],
        "weather": ["dry", "wet", None, "dry"],
    },
    "future_covariates": {
        "promotion": [True, False, False],
        "weather": ["dry", "wet", "snow"],
    },
    "schema": {
        "sales": {"type": "uint", "maximum": 10000},
        "promotion": {"type": "bool"},
        "weather": {"type": "enum", "categories": ["dry", "wet", "snow"]},
    },
}}
with Client() as client:
    forecast = client.forecast(inputs=inputs, horizon=3)
print(forecast["store-42"]["sales"].type)  # uint
print(forecast["store-42"]["sales"].median)
```

| Type | Input values | Target output |
| --- | --- | --- |
| `float` (default) | Finite numbers or `None` | Numeric quantiles |
| `int` | Signed integers or `None` | Rounded integer quantiles |
| `uint` | Nonnegative integers or `None` | Nonnegative integer quantiles |
| `bool` | `True`, `False`, or `None` | Boolean quantiles, not class probabilities |
| `enum` | Declared string categories or `None` | Covariates only; enum targets are rejected |

Numeric `minimum` and `maximum` validate supplied values and constrain every
returned quantile. Integer observations and bounds must be exactly representable
in model precision; these semantic types do not specify storage bit widths.
Booleans use 0/1 internally and target quantiles are thresholded at 0.5.

Enums require 2–32 unique categories. Each becomes an indicator channel; limits
apply after expansion. Unknown labels are rejected, and missing observations
remain missing in all indicator channels. The same declared vocabulary applies
to matching past and future covariates. No type is inferred from observed values.

Bounds, integer rounding and boolean projection apply to output quantiles,
including streamed chunks; they do not alter internal rollout feedback.
Output-only `constraints` remain available in the HTTP contract, but a target
cannot have both `schema` and `constraints`. Prefer `schema` for typed variables.

## Work with the forecast object

Every ordinary forecast method returns `ForecastResult`. This complete example
requires the `arrays` and `pandas` extras:

```python
from flashpdm import Client

with Client() as client:
    forecast = client.forecast(
        inputs={"store-42": {"targets": {"sales": [120, 132, 125, 141]}}},
        horizon=3, quantile_levels=[0.1, 0.5, 0.9],
    )

frame = forecast.to_pandas()
values = forecast.to_numpy()
data = forecast.to_dict()
csv_text = forecast.to_csv()
forecast.to_csv("forecast.csv")
print(frame)
print(values.shape)  # (1, 1, 3, 3)
```

| Conversion | Result and ordering |
| --- | --- |
| `to_pandas()` | Rows indexed by `(input_id, target, step)`; numeric quantile levels are columns. Request metadata and target types are in `DataFrame.attrs`. |
| `to_numpy()` | Float64 `(input, target, step, quantile)`. Input order and the first input's target order are retained; other inputs align by target name. |
| `to_dict()` | Independent dictionary in the public response format, including types and metadata. |
| `to_csv()` | CSV text with `input_id,target,step` followed by quantile-level columns. No pandas dependency. |
| `to_csv(path_or_buf)` | Write the same CSV to a path or writable text stream; return `None`. |

Steps are zero-based forecast positions, not inferred timestamps. Streaming chunks
retain absolute offsets. NumPy requires identical target-name sets across inputs;
use pandas/dictionary/CSV for heterogeneous targets. There is no silent padding or
dropping of targets. Dense float64 arrays represent boolean values as 0/1;
dictionaries preserve actual booleans.

`ForecastResult` exposes `request_id`, `model`, `revision`, `horizon`,
`quantile_levels`, `forecasts`, `metadata`, `offset`, and `chunk_horizon`.
`forecast[input_name][target_name]` returns a `QuantileForecast` with `type`,
`levels`, `quantile_values` shaped `(Q,H)`, `median`, `quantile(level)`,
`interval(coverage)`, and `to_numpy()` returning `(levels, values)`.

## Pandas DataFrames and Series

Importing `flashpdm.pandas` explicitly registers `.flashpdm` on DataFrames and
Series. Importing `flashpdm` alone does not import pandas or NumPy or register an
accessor. The accessor requires an explicit synchronous client:

```python
import pandas as pd
import flashpdm.pandas
from flashpdm import Client

frame = pd.DataFrame({
    "date": pd.date_range("2026-01-01", periods=4, tz="UTC").strftime("%Y-%m-%dT%H:%M:%SZ"),
    "sales": [120, 132, 125, 141],
})
with Client() as client:
    forecast = frame.flashpdm.forecast(
        client=client, targets=["sales"], time_column="date", horizon=3,
        schema={"sales": {"type": "uint"}},
    )
    single = frame["sales"].flashpdm.forecast(client=client, horizon=3)
print(forecast.to_pandas())
print(single["input-0"]["sales"].median)
```

Without accessor registration, call
`client.forecast_dataframe(frame, targets=["sales"], horizon=3)`.
Columns must have unique nonempty string names. Indexes are not sent: call
`reset_index()` and explicitly map a column if your index contains timestamps or
input names. A Series uses its string name, or `value` if unnamed, and row order.

`id_column` groups rows into independent inputs; without it, the input is named
`input-0`. `time_column` validates increasing, regularly spaced ISO timestamps
with an explicit timezone. Rows are not sorted, resampled or deduplicated.

## CSV upload and future values

CSV uploads accept a path or readable text/binary file. This example supplies
four historical rows followed by three known-future temperature values:

```python
from io import StringIO
from flashpdm import Client

csv = StringIO(
    "sales,temperature\n"
    "120,18\n132,19\n125,20\n141,21\n"
    ",22\n,21\n,20\n"
)
with Client() as client:
    forecast = client.forecast_csv(
        csv, targets=["sales"], past_covariates=["temperature"],
        future_covariates=["temperature"], history_rows=4, horizon=3,
        schema={"sales": {"type": "uint"}},
    )
print(forecast["input-0"]["sales"].median)
```

Column mappings explicitly select roles. With future covariates, `history_rows`
sets the cutoff per input, followed by exactly `horizon` rows. Future target cells
must be empty. Matching past/future names select one column; future columns also
supply their history. Leave historical cells empty when unknown. Empty cells mean
missing values; boolean cells accept true/false case-insensitively and enum labels
must match exactly. A shared `schema` applies to every input group.

CSV and DataFrame upload methods are synchronous. See the
[HTTP CSV reference](https://www.flashmind.com/docs/api) for multipart details.

## NumPy inputs and shapes

Install the `arrays` extra. This example forecasts two independent univariate
inputs, preserving their order:

```python
import numpy as np
from flashpdm import Client

history = np.array([[[120, 132, 125, 141]], [[90, 95, 93, 101]]])
with Client() as client:
    forecast = client.forecast_numpy(
        history, horizon=3, target_names=["sales"],
        schema={"sales": {"type": "uint"}},
        quantile_levels=[0.1, 0.5, 0.9],
    )
print(forecast.to_numpy().shape)  # (2, 1, 3, 3)
print(forecast["input-1"]["sales"].median)
```

Target shapes are `(T,)`, `(V,T)` or `(N,V,T)` for time, targets and independent
inputs. Covariates use `(N,C,T)` for history and `(N,C,H)` for future; omit the batch
axis for one input. Pass `past_covariates=`, `future_covariates=`, and
`covariate_names=`. Past/future array covariates must have equal widths and variable
ordering; use named JSON for differing variable sets.

Names default to `input-0`, `target-0`, `covariate-0`, etc. A shared `schema` uses
those names or the supplied variable names. NaN becomes missing, infinity is
rejected, and input numeric precision is preserved before server validation.
Boolean/string arrays require matching schema declarations; mixed variable types
may require an object array. History is never silently truncated.

`forecast_quantiles(values, horizon, **options)` is a convenience adapter returning
float32 `(N,V,H,Q)` directly. Prefer `forecast_numpy(...).to_numpy()` when you also
need labels, types or metadata.

## Async requests and streaming

`AsyncClient` uses the same request structure and returns the same objects:

```python
import asyncio
from flashpdm import AsyncClient

async def main():
    inputs = {"store-42": {"targets": {"sales": [120, 132, 125, 141]}}}
    async with AsyncClient() as client:
        forecast = await client.forecast(inputs=inputs, horizon=3)
        print(forecast["store-42"]["sales"].median)

asyncio.run(main())
```

In a notebook with an event loop, use `await main()` instead of `asyncio.run()`.
Async methods include `forecast`, `forecast_numpy`, `forecast_quantiles`, and
`stream`. There are no async CSV/DataFrame upload methods in this release.

Streaming delivers validated `ForecastResult` chunks:

```python
from flashpdm import Client

inputs = {"store-42": {"targets": {"sales": [120, 132, 125, 141]}}}
with Client() as client:
    chunks = client.stream(inputs=inputs, horizon=3)
    try:
        for chunk in chunks:
            print(chunk.offset, chunk.chunk_horizon)
            print(chunk["store-42"]["sales"].median)
    finally:
        chunks.close()
```

Append chunks along the time axis. Successful iteration requires a matching
completion event; an error/disconnect means the overall forecast is incomplete.
Never combine partial chunks with a retry's output. Closing an unfinished iterator
cancels the HTTP request. For async streaming use `async for chunk in
client.stream(...)`, then `await chunks.aclose()` if stopping early.
Streaming changes delivery, not the forecasting algorithm.

## Rollout and context options

| Argument | Behavior |
| --- | --- |
| `model="pdm-nano"` | Default; also accepts `pdm-pico` and `pdm-femto`. |
| `quantile_levels=[...]` | Select increasing levels within [0.01, 0.99]; default is all 21. |
| `rollout={"mode": "auto"}` | Default: direct forecast up to 960 steps, recursive median feedback beyond that, subject to account limits. |
| `rollout={"mode": "direct"}` | Reject horizons beyond 960 rather than recursively extending. |
| `context={"truncate": True}` | Explicitly retain the latest observations when history exceeds the context limit; otherwise reject. |
| `revision="..."` | Require an exact supported revision returned by a prior API response. |
| `idempotency_key="..."` | Authenticated requests only; prevent duplicate admission. Does not retrieve a stored result. |
| `compress=False` | Disable automatic request gzip for JSON payloads of at least 64 KiB. |

These options are keyword arguments to `forecast()` and array adapters. CSV
uploads accept applicable forecast options alongside column mappings. The model
uses sign-flip averaging as a fixed serving behavior; there is no ensemble
selector. Recursive forecasts do not propagate full trajectory uncertainty.

## Errors, retries and timeouts

Handle failures explicitly and retain the request ID when contacting support:

```python
from flashpdm import APIError, Client

with Client(timeout=750, max_retries=2) as client:
    try:
        forecast = client.forecast(
            inputs={"store-42": {"targets": {"sales": [120, 132, 125, 141]}}},
            horizon=3,
        )
        print(forecast["store-42"]["sales"].median)
    except APIError as error:
        print(error.code, error.status_code, error.request_id, error.path)
        raise
```

`APIError` exposes `status_code`, `code`, `request_id`, `path`, and `retry_after`.
Transport errors can have no HTTP status; paths are JSON Pointers. Invalid local
arguments may raise `ValueError` or `TypeError` before a request is sent.

The client retries connection-establishment failures and known pre-admission
rate/concurrency rejections up to twice by default. It also retries
`model_not_ready` and `gateway_busy` (rejected before prediction admission). Backoff includes jitter and honors Retry-After. Delays exceeding
60 seconds are returned as errors rather than shortened. Set `max_retries=0` to
disable retries; configurable values are 0–10.

Read/write failures, 504 responses, unknown server errors, quota exhaustion,
malformed output and partial streams are not automatically replayed: execution
may already have happened. Each admitted request consumes allowance even if it
fails. Authenticated idempotency keys prevent duplicate admission, but duplicate
requests return an error rather than replaying a stored prediction.

`503 admission_unknown` means the server could not confirm admission before its
deadline. No forecast was started, but the allowance write may have committed.
The client does not automatically retry this response. Retain `error.request_id`
when checking the request; submitting new work may consume another call.

The default timeout is 750 seconds to accommodate a cold startup. It configures
HTTP transport phase timeouts, not a guaranteed total request duration. Warm
requests return immediately when complete. The server bounds authentication and
public traffic admission at 5 seconds each, prediction admission at 10 seconds,
and model execution at 735 seconds after admission. These are separate phase
budgets, not one overall HTTP deadline.
`base_url=` accepts HTTPS endpoints or loopback HTTP for development. Response
compression is decoded automatically; CSV uses multipart and SSE stays uncompressed.

## Method reference

| API | Returns |
| --- | --- |
| `Client.forecast(*, inputs, horizon, **options)` | `ForecastResult` |
| `Client.forecast_csv(file, *, targets, horizon, **options)` | `ForecastResult` |
| `Client.forecast_dataframe(frame, *, targets, horizon, **options)` | `ForecastResult` |
| `Client.forecast_numpy(values, horizon, **options)` | `ForecastResult` |
| `Client.forecast_quantiles(values, horizon, **options)` | Float32 NumPy `(N,V,H,Q)` |
| `Client.stream(*, inputs, horizon, **options)` | Iterator of `ForecastResult` chunks |
| `AsyncClient.forecast(...)` / `forecast_numpy(...)` | Awaitable `ForecastResult` |
| `AsyncClient.forecast_quantiles(...)` | Awaitable NumPy array |
| `AsyncClient.stream(...)` | Async iterator of `ForecastResult` chunks |
| `frame.flashpdm.forecast(client=..., targets=..., horizon=..., **options)` | `ForecastResult` |
| `series.flashpdm.forecast(client=..., horizon=..., **options)` | `ForecastResult` |

Use context managers to close clients, or call `client.close()` /
`await client.aclose()` explicitly. Forecast operations return validated objects;
requesting CSV output is a local `forecast.to_csv()` conversion.

## Parallel forecasting

Install `flashpdm[batch]` for `ForecastPool`, a bounded process pool for numeric
windows of different lengths and channel counts. It has no dataset or scoring
dependencies; benchmark adapters remain outside the client.

```python
import numpy as np
from flashpdm import Client
from flashpdm.batching import ForecastPool

contexts = [np.array([10, 12, 11]), np.array([4, 5, 7, 6])]
with Client() as client:
    with ForecastPool(client, concurrency=4, cache_dir="forecasts") as pool:
        forecasts = list(pool.forecast_quantiles(
            contexts, 12, quantile_levels=[0.1, 0.5, 0.9],
        ))
        cached = pool.cached()

# One raw array per input, in input order: [target, horizon, quantile].
print(forecasts[0].shape)
```

Use `[time]` for a univariate context or `[target, time]` for multivariate input.
`past_covariates=` accepts a paired iterable of `[channel, time]` arrays or `None`.
`future_covariates=` accepts paired `[channel, horizon]` arrays. Past and future
arrays use the same channel order; use NaN for channels with no known future
values. Both are included in the forecast cache identity.
History is aligned, compatible inputs are padded together, and oversized inputs
fail explicitly. `BatchLimits` controls request size and bounded lookahead.
The pool reads model capabilities from the API and pins a revision on first use;
pass `revision=` to use an existing revision. Set `model=` for other PDM models.

Multiple callers share the same `concurrency` limit and reusable connections.
Choose concurrency to fit your API allowance. `Client` timeout and retry settings
also apply to each worker. The default `max_attempts=1` adds no whole-request
replays beyond the client's retry policy. Setting `max_attempts=2` or `3` explicitly
allows complete-batch replay after uncertain transport/server errors; earlier
work may have run, so this can repeat computation and consume allowance.

`cache_dir=` saves validated batches atomically. Identity includes the model,
revision, inputs, padding, covariates, quantiles, horizon and rollout policy.
Reopening the same cache reuses matching forecasts; corrupt entries fail rather
than being silently replaced. `pool.stats` reports saved, reused and failed
batches. This is an optional local cache, not a server cache.

`pool.cached()` returns a credential-free reader suitable for CPU scoring
processes. Use `with cached:` and the same `forecast_quantiles(...)` call to read
saved forecasts. Missing or corrupt batches fail; the reader cannot make API
requests. The pool returns raw quantiles without rounding, nonnegative clipping,
metric calculation or benchmark-specific transformations.

For named targets and covariates, use `pool.forecast_inputs(...)`:

```python
from flashpdm import Client
from flashpdm.batching import ForecastPool

inputs = [{
    "targets": {"demand": [10, 12, 11]},
    "past_covariates": {"price": [2, 2, 3]},
    "future_covariates": {"price": [3, 4]},
}]
with Client() as client, ForecastPool(client, concurrency=4) as pool:
    forecasts = list(pool.forecast_inputs(inputs, 2, quantile_levels=[0.1, 0.5, 0.9]))
    print(forecasts[0]["demand"].quantile_values)
```

This method preserves variable names, schemas, and distinct past/future covariate
sets, and yields each input's target forecasts in input order. It batches within
`BatchLimits` and uses the same concurrency and recovery policy. It does not
truncate, encode, or pad inputs, and does not support `cache_dir`; numeric cached
forecasting remains available through `forecast_quantiles(...)`.
