Adding a public API component¶
This page is the contributor contract for adding any public component to
PanorAi. It defines whether the behavior belongs in Geometry, typed data, an
operation or Torch layer, an estimator/policy, a Pipeline Stage, a subsystem,
or a task-level Application; how it composes with
panorai.pipeline, when an operation or Torch layer belongs in
panorai.ops.spherical, and which semantics and evidence must accompany the
change.
Research evaluators are a separate role: they compare existing outputs against independently defined evidence and must not be hidden inside the algorithm they measure.
The short rule is: one algorithm, several deliberate adapters. A wrapper
must reuse the implementation owned by its domain package; it must not copy an
algorithm into panorai.applications. Projection geometry continues to be
owned by panorai.geometry and the Stable object workflow.
Stability is attached to each public symbol, not to its namespace. The generic Pipeline, general NumPy spherical operations, and the SLIC vertical are Stable. The generic application framework, DoG, Stereo, Relative Pose, Mask R-CNN Paper, and CAM/SAM stages remain Experimental until their individual promotion gates pass. The optional generic Torch layer grammar is Stable on CPU; other device backends retain separate evidence requirements.
Choose the public artifact¶
An application may expose more than one artifact, but each has a different responsibility.
Artifact |
Add it when |
Required public shape |
Composes through |
|---|---|---|---|
Domain algorithm |
The mathematics already has an owning package such as
|
Existing domain API and result type. |
Called by adapters; never copied. |
Geometry |
The behavior defines coordinates, rays, projection, back-projection, support, or a canonical spatial transformation. |
Explicit frames, axes, units, pixel centres, support and validity under
|
|
Typed data or result |
Composition needs to preserve modality, grid, units, support, validity, observations or diagnostics beyond a bare array. |
Immutable public value with validated construction and named semantics. |
Passed between Stages, operations, estimators and Applications. |
Functional operation |
Stateless NumPy users need the primitive directly. |
|
|
Configured operation |
The primitive needs immutable parameters, grid/mask semantics, or use
inside |
Callable frozen object with |
|
|
A step changes execution domain, consumes a typed request, or returns a structured/non-raster result. |
|
|
|
Existing stages express the complete low-level route. |
|
Nesting, branching, provenance, persistence and cache. |
|
Users need a task-level call with domain language and a typed result. |
Immutable wrapper implementing |
Direct high-level use. |
|
The task-level call has an equivalent one-input composition. |
|
High-level use and inspectable low-level composition. |
Estimator or policy |
Observations are used to infer a model, score a hypothesis, choose a strategy, or accept/abstain; the object is not an image filter. |
Immutable options plus a typed result/diagnostic object; randomness and failure behavior are explicit. |
May be called directly or adapted by a thin public |
Research evaluator |
Existing outputs must be compared under an independently frozen metric, split and threshold protocol. |
Geometry-aware metric plus immutable typed result, fingerprinted protocol and reproducible record. |
Called after an operation, estimator, Pipeline or Application; never absorbed into the implementation under test. |
Torch layer/module |
The operation is differentiable, learned, or belongs in
|
|
|
Do not make one object change category according to a parameter. For example,
an application must not sometimes return a Pipeline and sometimes execute it.
The application always returns the documented result; as_pipeline()
always returns the inspectable composition.
An application is not automatically a Torch layer. When both are useful,
publish the layer or layer factory separately under
panorai.ops.spherical.nn and let the application compose it. Only
as_pipeline() is standardized today. A public build_module() or
layer convenience may be proposed, but it is application-specific until a
separate contract is accepted; it must return a module that passes
panorai.ops.spherical.nn.validate and it must not bypass grid or mask
propagation.
Application contract¶
Use a frozen dataclass for built-in applications. This makes
get_params() and immutable with_params() useful without a second
configuration system.
The general contract is:
from typing import Any
class Application:
application_id: str
def run(self, *args: Any, **kwargs: Any) -> Any: ...
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
def get_params(self, *, deep: bool = True) -> dict[str, Any]: ...
def with_params(self, **params: Any) -> "Application": ...
def describe(self) -> dict[str, Any]: ...
def to_config(self) -> dict[str, Any]: ...
def save(self, path) -> path: ...
class PipelineApplication(Application):
def as_pipeline(self) -> Pipeline: ...
run is intentionally variadic because task-level inputs differ. New
applications should select one of these signatures and document it exactly:
__call__ delegates to run. with_params(name=value) returns a new
instance of the same concrete frozen dataclass; it never mutates the original.
Nested dataclass configuration uses parent__child=value and unknown paths
fail explicitly. Do not add a second mutable set_params surface.
Unary ERP¶
Use for one-panorama processing. An array may accept an explicit validity
mask; an EquirectangularImage already owns its support and validity.
def run(
self,
panorama: ArrayOrEquirectangularImage,
*,
validity_mask: BooleanHW | None = None,
) -> PublicResult: ...
Unary ERP with external support¶
Use only when the source itself can be geometrically unsupported, as in an adapter that receives a cropped or sensor-limited raster.
def run(
self,
panorama: ArrayOrEquirectangularImage,
*,
support_mask: BooleanHW | None = None,
validity_mask: BooleanHW | None = None,
) -> PublicResult: ...
Never accept masks alongside a container and silently override the container. Fail explicitly, as the existing wrappers do.
Calibrated multi-input¶
Use for stereo or another task whose public inputs are naturally separate. State frame and units in the signature documentation.
def run(
self,
reference: Array,
source: Array,
rotation_source_from_reference: Array,
translation_source_from_reference: Array,
) -> PublicResult: ...
A Pipeline consumes one value. Bridge a multi-argument wrapper to the pipeline with an immutable typed request:
@dataclass(frozen=True, slots=True)
class StereoInput:
reference: Array
source: Array
rotation_source_from_reference: Array
translation_source_from_reference: Array
class StereoStage:
input_kind = "stereo_pair"
output_kind = "result"
def execute(self, value: StereoInput) -> StageResult:
if not isinstance(value, StereoInput):
raise TypeError("StereoStage requires StereoInput")
result = estimator(value)
return StageResult(result, {"domain": "sphere", "quantity": "radial_range"})
The high-level run creates StereoInput and calls as_pipeline().
This preserves an ergonomic user signature without adding variadic semantics
to Pipeline.
Capture policy versus result acceptance¶
Keep pre-estimation capture eligibility separate from post-estimation result
acceptance. A capture policy consumes explicit acquisition metadata in a typed
request and may precede feature extraction as a Stage. A result policy
consumes estimator diagnostics after computation. Never infer capture overlap,
metric baseline, motion, or frame rate from RGB values, and never present a
development envelope as a cross-domain guarantee. The relative-pose
application demonstrates this split with RelativePoseCaptureInput,
AssessRelativePoseCapture, and RelativePoseAcceptancePolicy.
Stateful or streaming application¶
Do not force a mutable session into PipelineApplication. Keep immutable
configuration in an application/factory and return a separately named session
object with explicit lifecycle methods. State, reset behavior, thread safety,
serialization, and bounded-memory behavior require their own contract.
Wrapper skeleton¶
from dataclasses import dataclass
from typing import Any
from panorai.applications import PipelineApplication
from panorai.applications._base import application_config # built-ins only
from panorai.pipeline import OnSphere, Output, Pipeline, RasterField
@dataclass(frozen=True, slots=True)
class SphericalExample(PipelineApplication):
strength: float = 1.0
application_id = "panorai.applications.SphericalExample"
def _operation(self):
# Return the domain-owned configured operation. Do not reimplement it.
return ExampleOperation(strength=self.strength)
def as_pipeline(self) -> Pipeline:
return Pipeline(
OnSphere(
Output(self._operation(), kind="confidence"),
output="confidence",
)
)
def run(self, panorama: Any) -> RasterField:
result = self.as_pipeline()(panorama)
if not isinstance(result, RasterField):
raise RuntimeError("unexpected SphericalExample result")
return result
def describe(self) -> dict[str, Any]:
return {
"interface": self.interface,
"stability": self.stability,
"application": self.application_id,
"input": "canonical ERP with explicit support/validity",
"output": "RasterField(kind='confidence')",
"domain": "direct-spherical",
"pipeline": self.as_pipeline().describe(),
}
def to_config(self) -> dict[str, Any]:
return application_config(self.application_id, self.get_params(deep=False))
The example omits from_config and registry changes only for brevity. A
serializable built-in application must implement them as described below.
Stage contract¶
Use a Stage when the work is not just a raster-to-raster spherical operation. The complete public signature is:
class ExampleStage:
input_kind = "erp"
output_kind = "result"
def execute(self, value: Any) -> StageResult:
result = domain_algorithm(value)
return StageResult(
result,
{
"domain": "sphere",
"algorithm": "example",
"result_count": len(result),
},
)
input_kind and output_kind may be a string or tuple of strings. Use
the established kinds (erp, views, view_results, bundle,
result and any) when they fit. Introduce a new kind only with tests
for every legal and illegal transition. execute must return
StageResult; returning the raw value is an error. Details must be an
immutable-friendly, JSON-explainable mapping. Supply grid explicitly for
spatial values that do not carry a public grid themselves.
A Pipeline is already a Stage. Prefer nesting Pipeline over a private
executor or a second orchestration loop. Use Parallel for logical
branches, Select for a dotted result path, and Attach only when raster
grids are compatible. Parallel and PerView.parallel describe
topology; they do not promise threads.
Sphere-native NumPy operation contract¶
Put the stateless primitive in panorai.ops.spherical.functional when it is
useful by itself. Put the immutable configured adapter in
panorai.ops.spherical when it must run through OnSphere. The adapter
delegates to the primitive or an existing domain implementation.
For a spatial raster result, the configured object provides:
@dataclass(frozen=True, slots=True)
class ExampleOperation:
parameter: float
spherical_operation = True
preserves_shadow_state = True
def __call__(self, values: np.ndarray) -> np.ndarray: ...
def transform_grid(self, input_grid: RasterGrid) -> RasterGrid: ...
def transform_mask(self, mask: BooleanHW) -> BooleanHW: ...
def describe(self) -> dict[str, Any]: ...
Shape-preserving operations still append an auditable grid transform. A
shape-changing operation must compute the exact first sampled centre, stride,
resolution, periodicity, and transform history. It may return to canonical
erp-pixel-centres only after explicit regridding at those rays.
If an operation supports validity renormalization, implement:
def apply_with_masks(
self,
values,
support,
validity,
*,
invalid_policy: str,
min_valid_weight: float | None,
) -> tuple[value, support, validity]: ...
Renormalization is valid only when its mathematics is defined. General learned convolution must use strict propagation. Labels and masks are nearest-only; integer or boolean bilinear interpolation must fail.
Projected-view callable contract¶
Use SampleViews plus PerView for an ordinary pinhole/model callable.
The callable receives one raw face array/tensor and may return:
an array/tensor;
(array_or_tensor, boolean_validity_mask); orpanorai.data.RasterFieldResult(data, validity, support).
Declare output semantics with Output. Raster kinds are image,
depth, labels, mask, logits, features and confidence.
Non-raster kinds are classification, embedding, detections and
keypoints. Depth always means radial range and requires units. A
non-raster per-view result retains face identity, projection, support and
validity.
For batch="auto", a callable must either be a Torch module or declare
supports_batch = True. Batched output must preserve a leading batch
dimension or return one item per input. There is no silent retry after OOM.
Torch layer contract¶
Torch is optional and must remain lazy. New direct-spherical layers belong in
panorai.ops.spherical.nn and use NCHW/CHW conventions documented by the
existing layer family. A model intended for OnSphere must satisfy:
def forward(self, values): ...
and must be composed from validated spherical spatial layers and permitted
pointwise layers. In the Stable contract, custom container forward
methods fail closed because hidden functional spatial calls cannot be proven
safe. Use torch.nn.Sequential or propose explicit
transform_grid, transform_mask and forward_with_masks support with
tests before exposing a custom container.
Before returning or accepting a module, run:
from panorai.ops.spherical import nn as spherical_nn
report = spherical_nn.validate(module, input_shape=(height, width))
The report must contain no forbidden planar spatial layer. Grid and mask transforms must agree with the actual output shape, and device, dtype, layout, batch order and autograd must be preserved.
An application that uses a layer should normally accept the configured module as a component or construct it through a clearly named, deterministic factory. Do not make model acquisition, checkpoint download or license acceptance a side effect of importing or constructing the application.
Result and modality contract¶
Choose the narrowest public result that preserves semantics:
use
RasterFieldfor a pipeline raster, including values, kind, units, support, validity andRasterGrid;use
ClassificationResultorEmbeddingResultfor those global or per-view results;use
PerViewResults/ViewResultfor detections or keypoints that must retain view geometry;use
SphericalObservationsafter lifting observations into panorama-frame bearings;use
ResultBundlefor named mixed outputs;reuse a domain result type when it already expresses the complete contract.
Never return a bare spatial tensor when its grid differs from the input. Do not infer support or validity from the numeric value. Black RGB, zero labels and zero radial range can all be valid values. Floating non-finite data may not silently become valid zero.
Every application specification must state:
input domain, shape/layout, dtype and backend;
coordinate frame and pose direction;
output type, grid and units;
radial range versus any explicitly named alternative depth quantity;
support and validity propagation;
interpolation/fusion for every raster kind;
batch, device and autograd behavior;
deterministic versus stochastic behavior and seed ownership;
optional dependencies, model/data licenses and absence behavior;
external weights or resources and their identity/checksum policy;
stability tier, evidence and known failure modes.
Configuration, persistence and cache¶
describe() is explanatory and must be JSON-friendly. to_config() is
an executable persistence contract and is stricter.
For a serializable built-in application:
use a globally unique, stable
application_id;emit
application_config(application_id, parameters);implement
from_configwith type and value validation;add the class to the explicit allowlist in
Application.load;add round-trip, unknown-id, non-finite, oversized-file and atomic-write tests.
Do not place an import path from the file into a dynamic importer. An
application containing a lambda, arbitrary callable, unregistered model or
external adapter may execute in memory, but save() must fail with the exact
configuration path. This is the expected behavior for model-driven wrappers
until their components have safe registrations.
A new built-in Stage or configured operation is not automatically accepted by
Pipeline persistence. Add it to the explicit registry in
panorai.pipeline._serialization only after defining a safe parameter codec
and round-trip tests. Never use pickle.
For a custom Torch architecture, trusted application code may call
register_torch_module(identifier, module_type, encode=..., decode=...).
Registration is process-local and must occur before loading. A custom module
used directly by OnSphere is rejected unless it is built entirely from
validated layers or its registration explicitly declares
spherical_semantics="pointwise". That declaration is a trusted assertion
by application code, not an inference by PanorAi; it is only valid for modules
that preserve every spatial sample independently. Forward hooks are rejected
because they can change output semantics after validation. Topology remains
canonical JSON and weights use checksum-bound safetensors. Loading never
imports a class named by the archive.
Cache identity must cover values, support, validity, grid, dtype, layout, backend, device, version, configuration and registered weight identity. Do not cache a training module or a tensor requiring gradients; cache must never detach autograd silently.
Documentation is part of the public contract¶
contracts/public-api-surfaces-v2.json is the single tracked registry for
stability and documentation coverage. Before exporting a new component:
add or extend its public surface record;
assign the surface to at least one role under
documentation.assignments;use the role route to update the catalog, signature reference, concept and progressive guide;
register every concrete Application under
application_components;run
python scripts/sync_public_catalog.pyand commit the byte-identical resources underpanorai/catalog;add an installed-wheel consumer for every Stable Application;
feature the component in
documentation.readme.featuredonly when it is an intentional README entry point; every other surface remainscatalog-only;add a reasoned namespace exemption only for a gateway or implementation module that has no independent public contract; freeze its exact
__all__and name the audited surfaces that represent those symbols.
The role catalogs contain generated panorai-api-components tables. Do
not hand-copy the exhaustive surface list: a new registered surface appears in
its catalog automatically. CI fails when a public package namespace declares
__all__ but is neither audited nor explicitly exempted, when a surface has
no role, when a routed document is missing, when a Stable Application lacks a
source example or installed consumer, or when a featured README token/link is
absent.
GitHub Pages is built warning-strict from main. Changes under
contracts/** and to README.md are deployment inputs alongside
docs/**, public package code and the example runner. The Pages workflow
executes the source examples before upload and checks that the catalog,
reference, concept and guide entry routes exist in the rendered site.
Files, exports and documentation¶
For a normal built-in application, the change usually touches:
panorai/applications/_example.py high-level wrapper
panorai/applications/__init__.py explicit public export
panorai/<domain>/... one algorithm implementation
panorai/ops/spherical/... optional operation/layer adapter
panorai/pipeline/... only if a reusable Stage is needed
tests/test_example_application.py wrapper and real composition tests
docs/guides/... user route and limitations
docs/reference/... exact public names/signatures
docs/reference/catalog/... role, input/output and composition
contracts/public-api-surfaces-v2.json stability inventory
Also update the application load registry and Pipeline/Torch registries only
when persistence is intentionally supported. Do not add a root
panorai export merely for convenience; preserve import isolation and use
the narrowest owning namespace.
The wrapper must show both forms in documentation:
result = SphericalExample(...)(panorama)
result = SphericalExample(...).as_pipeline()(panorama)
The two forms must be equivalent for the wrapper’s documented inputs. The low-level form may require a typed request for a multi-input application.
Catalog registration is part of the public change. Add the component to the
curated role page—Application, Stage, operation, estimator, typed result or
Geometry—and state its canonical import, tier, input, output, execution domain
and common composition. The exhaustive
public-surface inventory is
generated from contracts/public-api-surfaces-v2.json; do not hand-copy its
symbol list. A new export is incomplete if it appears only in __all__ or
only on an automodule page.
Required tests and evidence¶
Before review, cover all applicable rows:
the wrapper uses the real domain implementation and returns the declared public result;
wrapper(...)andwrapper.as_pipeline()(...)agree;with_paramsis immutable and preserves the concrete wrapper type;every legal/illegal Stage transition fails or succeeds before inference;
support, validity and numeric values are tested independently;
seam, poles, partial support and all-invalid regions are considered;
black RGB, label zero, valid zero range and invalid
NaNare distinct;NumPy
HW/HWCand applicable TorchCHW/NCHWcases are covered;labels/masks remain nearest-only and depth has explicit radial units;
batching preserves order, dtype, device and autograd;
optional imports stay lazy and missing dependencies fail with actionable guidance;
configuration/pipeline round trips pass for registered components and fail explicitly for unregistered components;
public exports and stability inventory agree;
the role catalog identifies the new component and the generated exhaustive inventory includes its public surface;
examples execute, strict Sphinx has zero warnings, and an installed-wheel consumer runs outside the checkout before any Stable promotion.
Algorithmic correctness requires an independent oracle. Scientific applications also need frozen prospective criteria and eligible held-out data; unit tests and successful adapter execution are not sufficient promotion evidence. Model weights, datasets and training orchestration remain external unless redistribution and scope are separately approved.
Review checklist¶
A reviewer should be able to answer yes to each question:
Is there one mathematical implementation rather than a copy in the wrapper?
Is the chosen artifact level the smallest one that expresses the behavior?
Are the high-level and low-level routes equivalent and inspectable?
Are grid, support, validity, units and interpolation explicit?
Does every shape-changing spatial operation transform the grid correctly?
Are optional dependencies lazy and license/resource failures early?
Does persistence use explicit registrations and safe formats only?
Are public exports, stability tier, migration impact and limitations documented?
Do tests exercise real public objects and an independent oracle where correctness is claimed?
Is the evidence target named correctly: source, wheel, sdist, installed wheel or external service?
If any answer is no, keep the surface internal or Experimental and record the missing contract or evidence instead of widening the public promise.