Metadata-Version: 2.2
Name: henu-pyisis
Version: 1.2.11
Summary: Standalone pybind11 bindings for selected USGS ISIS APIs.
Author: Geng Xun
License: MIT License
         
         Copyright (c) 2026 Geng Xun, Henan University
         
         Permission is hereby granted, free of charge, to any person obtaining a copy
         of this software and associated documentation files (the "Software"), to deal
         in the Software without restriction, including without limitation the rights
         to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         copies of the Software, and to permit persons to whom the Software is
         furnished to do so, subject to the following conditions:
         
         The above copyright notice and this permission notice shall be included in all
         copies or substantial portions of the Software.
         
         THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         SOFTWARE.
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: C++
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Project-URL: Repository, https://github.com/guderianXu/pyisis
Requires-Python: >=3.11
Requires-Dist: usgs-pyisis-runtime-linux-x86-64-core==1.2.10; platform_system == "Linux" and platform_machine == "x86_64"
Requires-Dist: usgs-pyisis-runtime-linux-x86-64-libs-1==1.2.10; platform_system == "Linux" and platform_machine == "x86_64"
Requires-Dist: usgs-pyisis-runtime-linux-x86-64-libs-2==1.2.10; platform_system == "Linux" and platform_machine == "x86_64"
Requires-Dist: henu-pyisis-runtime-win-amd64-core==1.2.10; platform_system == "Windows" and platform_machine == "AMD64"
Requires-Dist: henu-pyisis-runtime-win-amd64-libs-1==1.2.10; platform_system == "Windows" and platform_machine == "AMD64"
Requires-Dist: henu-pyisis-runtime-win-amd64-libs-2==1.2.10; platform_system == "Windows" and platform_machine == "AMD64"
Requires-Dist: henu-pyisis-runtime-win-amd64-libs-3==1.2.10; platform_system == "Windows" and platform_machine == "AMD64"
Requires-Dist: henu-pyisis-runtime-win-amd64-libs-4==1.2.10; platform_system == "Windows" and platform_machine == "AMD64"
Description-Content-Type: text/markdown

<div align="right">

[English](./README.md) | [简体中文](./README.zh-CN.md)

</div>

Welcome to the USGS ISIS Python Bindings (i.e., PyISIS)!
This project wraps the powerful USGS ISIS (v9.0.0) photogrammetric software, enabling seamless integration with Python for planetary image processing.

🛠️ Installation: You can easily install the USGS ISIS package using Conda or Mamba. For step-by-step instructions, please see the [Environment Setup Guide](https://astrogeology.usgs.gov/docs/how-to-guides/environment-setup-and-maintenance/installing-isis-via-anaconda/).

📚 Learning Resources: Processing planetary images with ISIS can be complex. We strongly suggest checking out the [Getting Started Guide](https://astrogeology.usgs.gov/docs/how-to-guides/image-processing/) to navigate the learning curve effectively.

# pyISIS / `isis_pybind_standalone`

This repository provides Python bindings for **USGS ISIS 9.0.0**, built with `pybind11`, and currently delivers the `isis_pybind` extension module primarily for Linux.

The scope of this repository is intentionally clear:

- use a packaged Linux runtime wheel for binary pip installs, or an **already installed ISIS environment** as the external SDK / runtime for source builds
- build the Python-importable extension module `isis_pybind._isis_core`
- expose Python access to APIs used in planetary remote sensing, photogrammetry, control networks, camera models, projections, and geometry processing

> The Linux x86_64 binary distribution model is now a one-command pip install:
> `usgs-pyisis` pulls the matching split runtime shard wheels automatically.
> Source builds still require an external ISIS SDK/runtime through `ISIS_PREFIX`.

## Supported scope

The current recommended and validated compatibility range is:

| Item | Current recommendation / validated range |
| --- | --- |
| Operating system | Linux x86_64 |
| Python | CPython 3.12 |
| ISIS | USGS ISIS 9.0.0 runtime / development environment |
| Distribution mode | pip binary wheels for Linux x86_64, GitHub Release, source build |
| Recommended as a direct PyPI first release | Linux x86_64 wheel pair is supported experimentally; source builds still require external ISIS |

## What this repository builds

After a successful build, the core Python package directory is:

- `build/python/isis_pybind/`

It typically contains:

- `build/python/isis_pybind/__init__.py`
- `build/python/isis_pybind/_isis_core.cpython-312-x86_64-linux-gnu.so`
- `build/python/isis_pybind/LICENSE`

The actual bound shared library is:

- `_isis_core.cpython-312-x86_64-linux-gnu.so`

However, **do not copy and use only the `.so` file by itself**. It should live inside the `isis_pybind/` package directory together with `__init__.py`.

## For source builds, install USGS ISIS first

Source builds still require a **working ISIS environment**, preferably managed with conda / mamba.

### Recommended approach

Prepare an environment that already contains the ISIS 9.0.0 runtime and development files, for example the locally used environment:

- `asp360_new`

This project assumes only that the environment provides:

- `${CONDA_PREFIX}/include/isis`
- `${CONDA_PREFIX}/lib/libisis.so`
- `${CONDA_PREFIX}/lib/Camera.plugin`

In other words, any ISIS environment that provides those pieces can be used to build this binding.

### Suggested ISIS installation path

1. Use the **official USGS ISIS / Astrogeology** installation approach, or a conda recipe already validated by your lab or team.
2. Activate that environment.
3. Confirm that the following key paths exist:
   - `include/isis`
   - `lib/libisis.so`
   - `lib/Camera.plugin`

If those three items are missing, this project cannot be configured and linked successfully.

### About `ISISDATA`

- Many real camera, time, and geometry workflows still require `ISISDATA` at runtime.
- The repository tests will try to fall back to `tests/data/isisdata/mockup/` as a minimal mock environment.
- But if you are processing your own real imagery and camera models, you should prefer a properly configured real `ISISDATA` setup.

You can quickly inspect the active data tree from Python:

```python
import pyisis

with pyisis.use_isisdata("/path/to/ISISDATA"):
    print(pyisis.doctor_environment("lro").summary())

report = pyisis.doctor_spice(
    "example.echo.cal.cub",
    mission="lro",
    isisdata="/path/to/ISISDATA",
)
print(report.summary())
```

`doctor_environment()` reports both the selected ISIS runtime and the checked `ISISDATA` tree. `doctor_spice()` copies the cube to a temporary directory before trying `spiceinit`, so the original cube is not modified during diagnosis by default.
When ISIS reports a concrete missing kernel path, the diagnostic output also
includes the inferred mission, the relative `ISISDATA` path, and a
`downloadIsisData <mission> <ISISDATA>` sync hint.
For normal app calls, use `with pyisis.use_isisdata(...)` around a workflow or
pass `isisdata=...` to diagnostic app wrappers such as
`pyisis.spiceinit("example.cub", isisdata="/path/to/ISISDATA")`.

For scripts that need structured recovery steps instead of prose, call
`pyisis.diagnose_isisdata()` directly. The returned
`IsisDataDiagnostic` exposes deduplicated `missing_missions`,
`missing_relative_paths`, `sync_commands`, and a mission-grouped
`repair_plan` of `IsisDataRepairStep` records:

```python
diagnostic = pyisis.diagnose_isisdata(
    [
        "$lro/kernels/ck/missing.bc",
        "$lro/kernels/spk/missing.bsp",
    ],
    isisdata="/path/to/ISISDATA",
)
print(diagnostic.missing_relative_paths)
print(diagnostic.sync_commands)
for step in diagnostic.repair_plan:
    print(step.mission, step.command, step.relative_paths)
print(diagnostic.repair_script())
```

If you have the full native ISIS output instead of a hand-copied path, pass the
whole text to `diagnose_isis_output()`. It extracts wrapped
`Spice file does not exist [...]` paths before returning the same
`IsisDataDiagnostic` shape:

```python
diagnostic = pyisis.diagnose_isis_output(isis_log_text, isisdata="/path/to/ISISDATA")
print(diagnostic.summary())
```

For day-to-day ISISDATA checks, the dedicated `pyisis data` commands keep the
data-focused diagnostics separate from the broader runtime/app doctor:

```bash
pyisis data doctor \
  --mission lro \
  --isisdata /path/to/ISISDATA \
  --json

pyisis data missing-path '$lro/kernels/ck/missing.bc' \
  --isisdata /path/to/ISISDATA \
  --json

pyisis data diagnose-output spiceinit.log \
  --isisdata /path/to/ISISDATA \
  --json

pyisis data repair-script \
  --isisdata /path/to/ISISDATA \
  --missing-path '$lro/kernels/ck/missing.bc' \
  --output repair_isisdata.sh
```

These commands emit `"kind": "pyisis-data-doctor"`,
`"kind": "pyisis-data-missing-path"`, or
`"kind": "pyisis-data-output-diagnostic"` for machine-readable integrations.
`pyisis data repair-script` writes only the inferred `downloadIsisData` repair
commands when `--output -` is used, which is convenient for reviewable shell
snippets.

If you already have a missing path copied from an ISIS error log, the CLI can
produce the same repair-plan payload without opening a cube:

```bash
pyisis doctor --missing-path '$lro/kernels/ck/missing.bc' \
  --isisdata /path/to/ISISDATA \
  --json
```

The JSON payload uses `"kind": "pyisis-missing-path-diagnostic"` and places the
mission-grouped recovery steps under `diagnostic.repair_plan`.
Add `--write-repair-script repair_isisdata.sh` when you want pyisis to write a
reviewable bash script containing the inferred `downloadIsisData` commands:

```bash
pyisis doctor --missing-path '$lro/kernels/ck/missing.bc' \
  --isisdata /path/to/ISISDATA \
  --write-repair-script repair_isisdata.sh
```

For whole stderr or `print.prt` snippets, pass the text or a log file path with
`--isis-output`; JSON output uses `"kind": "pyisis-output-diagnostic"`:

```bash
pyisis doctor --isis-output spiceinit.log \
  --isisdata /path/to/ISISDATA \
  --json
```

All diagnostic reports returned by helpers such as `doctor_environment()`,
`doctor_spice()`, `runtime_report()`, and `diagnose_isisdata()` also expose
`.to_dict()` for notebook, API, and CI integrations. Use
`pyisis.diagnostic_to_dict(obj)` when you want to normalize either a pyisis
diagnostic report or a report-like object into the same JSON-ready shape:

```python
import json
import pyisis

report = pyisis.doctor_environment("lro", isisdata="/path/to/ISISDATA")
payload = report.to_dict()
print(json.dumps(payload, indent=2))
print(pyisis.diagnostic_to_dict(report)["ok"])
```

The same diagnostic path is available from the command line. Add `--json` when
CI jobs, notebooks, or GUI tools need machine-readable output; the JSON payload
contains fields such as `"environment"`, `"cube"`, `"spice"`, and
`"sync_commands"`:

After a fresh pip install, the shortest no-data readiness check is
`pyisis doctor --install-check`. It verifies the packaged runtime bootstrap,
common app bindings, and workflow app coverage without requiring `ISISDATA`.
Use `pyisis doctor --install-check --json` for CI; the JSON payload includes
`"kind": "pyisis-install-check"` and an overall `"ok"` field.
Installed smoke logs print `pyisis-doctor install check smoke: ok` after this
check passes; release gates use that marker to prove the no-data install check
ran before data-dependent workflow smoke.

```bash
pyisis doctor \
  --mission lro \
  --apps \
  --isisdata /path/to/ISISDATA \
  --cube example.echo.cal.cub \
  --json
```

Add `--apps` to check that common Python-facing ISIS app bindings such as
`spiceinit`, `cam2map`, and control-network helpers are callable. In Python,
the same check is available as `pyisis.doctor_app_bindings().summary()`.
To check only selected bindings, repeat `--app`, for example
`pyisis doctor --app spiceinit --app cam2map --json`.
To check the app coverage required by a high-level workflow, use `--workflow`,
for example `pyisis doctor --workflow lro-nac --json`.
In Python, `pyisis.check_workflow("lro-nac", isisdata="/path/to/ISISDATA")`
returns the same workflow-level readiness report, combining runtime,
mission-data, and app-binding diagnostics before the full workflow starts.
To discover supported workflow names and their required apps, run
`pyisis doctor --list-workflows --json` or inspect
`pyisis.workflow_app_bindings()` from Python.

`pyisis-doctor` remains available as a compatibility shortcut for the same
diagnostic path.

For a step-by-step workflow covering `No Camera Kernels found`,
`pyisis doctor`, `pyisis-doctor`, `spiceinit_with_diagnostics()`, and RAM-backed pipeline troubleshooting, see
the [ISISDATA troubleshooting guide](docs/isisdata_troubleshooting.md).

## Installing this binding: recommended options

### Option A: one-command pip install for Linux x86_64 wheels

When the main wheel and runtime shard wheels are available on the same package index, Linux x86_64 users can install the binding and the bundled runtime with one command:

```bash
python -m pip install usgs-pyisis
```

Internally, pip installs:

- `usgs-pyisis`
- `usgs-pyisis-runtime-linux-x86-64-core`
- `usgs-pyisis-runtime-linux-x86-64-libs-1`
- `usgs-pyisis-runtime-linux-x86-64-libs-2`

For local release validation, point pip at a wheel directory containing the main wheel and all runtime shard wheels:

```bash
python -m pip install --find-links /path/to/wheels usgs-pyisis
```

The runtime shard wheels provide `ISISROOT`, `libisis.so`, ISIS plugin/appdata files, and the non-system shared libraries collected from the build environment. Real workflows may still require a real `ISISDATA` tree.

Before running data-heavy workflows, use the no-data install-check path to
confirm that the wheel, bundled runtime, common app bindings, and workflow app
coverage are visible:

```bash
pyisis doctor --install-check --json
```

From a source checkout, `examples/pip/install_check.py` wraps the same checks
and also renders a sample missing-kernel repair plan with a
`repair script preview`, so users can verify diagnostics before downloading a
large `ISISDATA` tree:

```bash
python examples/pip/install_check.py \
  --isisdata /path/to/ISISDATA \
  --missing-path '$lro/kernels/ck/pyisis-install-check-missing.bc'
```

### RAM-workspace pipelines

`pyisis.Pipeline` provides a path-compatible workflow layer for chaining native ISIS apps while keeping intermediate cubes in a temporary workspace. On Linux, `workspace="ram"` prefers `/dev/shm` when it is writable, so large intermediate products avoid normal disk I/O pressure while still using the upstream ISIS app implementations.

```python
import pyisis

with pyisis.use_isisdata("/path/to/ISISDATA"):
    with pyisis.Pipeline(workspace="ram") as pipe:
        cube = (
            pipe.lronac2isis("input.IMG")
            .lronaccal()
            .lronacecho()
            .spiceinit(attach=False)
            .footprintinit(linc=100, sinc=100)
        )
        preview = cube.reduce(sscale=2, lscale=2).isis2std(format="PNG")
        print(cube.path)
        print(cube.shape)
        print(preview.path)
```

This is not a full in-memory rewrite of ISIS. It is a RAM-backed workspace wrapper around native path-based ISIS apps. Apps such as `spiceinit` and `footprintinit` operate in-place on the same cube handle; apps that naturally produce a new cube, such as `lronaccal`, `lronacecho`, `reduce`, and `cam2map`, still create output cubes, but those outputs can live in RAM-backed temporary storage and are cleaned up automatically when the context exits. Use `keep=True` while debugging to preserve the workspace.

Pipeline cube handles expose `samples`, `lines`, `bands`, and `shape` for quick
metadata checks. `shape` follows Python image conventions as
`(lines, samples, bands)`. When you need low-level `Cube` methods from a
pipeline product, `CubeHandle.open()` returns a context-managed cube view with
the same Pythonic metadata properties.

For low-level cube metadata reads outside a pipeline, use `pyisis.open_cube()`.
It accepts `str` and `pathlib.Path`, returns a context manager, and exposes
`path`, `samples`, `lines`, `bands`, and `shape` properties so you do not need
to construct `FileName` manually or remember the C++ method names.

```python
import pyisis

with pyisis.open_cube("example.cub", "r") as cube:
    print(cube.path)
    print(cube.samples)
    print(cube.lines)
    print(cube.bands)
    print(cube.shape)
```

High-level `pyisis` app wrappers and `Pipeline` methods report context-rich
failures as `pyisis.AppError`. The original ISIS exception remains available
through `error.__cause__`, while the Python error exposes the app name, input
path, output path when one exists, parameters, and ISISDATA diagnostics.

```python
import json
import pyisis

try:
    pyisis.footprintinit("missing.cub", linc=100)
except pyisis.AppError as error:
    print(error.app)
    print(error.source)
    print(error.output)
    print(error.parameters)
    print(json.dumps(error.to_dict(), indent=2))
    failure = pyisis.explain_failure(error)
    failure_payload = failure.to_dict()
    print(json.dumps(failure_payload, indent=2))
    print(failure.summary())
    print(error)
```

For common LRO NAC processing, `pyisis.workflows` adds a task-level wrapper
that keeps the intermediate handles visible while reducing boilerplate:

For the shortest common path, use `process_lro_nac()`. It creates a managed
RAM workspace, runs the standard LRO NAC chain, applies friendly projection
defaults such as `cam2map pixres=MAP` and `defaultrange=CAMERA`, and returns a
`LroNacStandardResult`. Without `output_dir`, the temporary workspace remains
available until you call `cleanup()`. With `output_dir`, key products are copied
there and the temporary workspace is cleaned by default.
Before the SPICE, footprint, or projection stages run, `process_lro_nac()`
records the same `pyisis.check_workflow("lro-nac", ...)` readiness report used
by the doctor CLI. If a later native app raises `pyisis.AppError`,
`error.workflow_readiness` and `error.to_dict()["workflow_readiness"]` show
whether the runtime, app bindings, and mission `ISISDATA` preflight were already
OK when the app failed.
On successful runs, the same report is available as `result.workflow_readiness`
and as `workflow_payload["workflow_readiness"]` after `result.to_dict()`.
The same preflight also appears in progress reporting. A callable `progress`
receives a first event with `event == "workflow-readiness"`, `workflow`,
`mission`, `ok`, `isisdata`, and the original `report`. With
`progress="print"`, the terminal line starts with
`pyisis workflow readiness ok: lro-nac` before the app start/finish lines.

```python
import json
import pyisis

result = pyisis.process_lro_nac(
    "M1498479327LE.IMG",
    isisdata="/path/to/ISISDATA",
    project=True,
    preview=True,
    map_pixel_resolution=100.0,
    progress="print",
)

print(result.cube.path)
print(result.preview.path)
print([app_result.app for app_result in result.app_results])
workflow_payload = result.to_dict()
print(result.workflow_readiness.summary())
print(json.dumps(workflow_payload["workflow_readiness"], indent=2))
print(json.dumps(workflow_payload, indent=2))
result.cleanup()
```

For a runnable pip quickstart that keeps the command-line surface small, use
the installed `pyisis lro-nac` command. It runs the same full LRO NAC path,
copies the final products to a persistent output directory, and writes a JSON
summary that can be attached to a notebook, CI job, or GUI run record:
The summary keeps quick-scan fields such as `workflow_readiness_summary`,
`app_sequence`, `workspace_path`, and `cleanup_requested`, while the terminal
prints the workflow readiness, app sequence, copied outputs, and the
workspace cleanup choice.

```bash
pyisis lro-nac \
  --img /mnt/e/testData/eq/M1498479327LE.IMG \
  --isisdata /mnt/e/ISISDATA \
  --output-dir outputs/lro_nac \
  --summary-json outputs/lro_nac/workflow_summary.json \
  --map-pixel-resolution 100.0 \
  --overwrite
```

When working from a source checkout before installation, the compatibility
wrapper `examples/pip/lro_nac_full_workflow.py` accepts the same options and
delegates to the installed package implementation:

```bash
python examples/pip/lro_nac_full_workflow.py \
  --img /mnt/e/testData/eq/M1498479327LE.IMG \
  --isisdata /mnt/e/ISISDATA \
  --output-dir outputs/lro_nac \
  --summary-json outputs/lro_nac/workflow_summary.json \
  --map-pixel-resolution 100.0 \
  --overwrite
```

Use an explicit `Pipeline` when you want step-by-step control over every app
call:

```python
import os
import pyisis

os.environ["ISISDATA"] = "/path/to/ISISDATA"

with pyisis.Pipeline(workspace="ram") as pipe:
    result = pipe.lro_nac_standard(
        "M1498479327LE.IMG",
        project=True,
        map_kwargs={"pixel_resolution": 100.0},
        cam2map_kwargs={"pixres": "MAP"},
        spice_kwargs={"shape": "ELLIPSOID"},
        footprint_kwargs={"linc": 100, "sinc": 100},
        preview=True,
        preview_reduce={"sscale": 4, "lscale": 4},
    )
    print(result.imported.path)
    print(result.calibrated.path)
    print(result.echo.path)
    print(result.projected.path)
    print(result.cube.path)
    print(result.preview.path)
    print([app_result.app for app_result in result.app_results])
```

`lro_nac_standard()` requires an explicit `Pipeline` so the lifetime of the
RAM-backed workspace is visible to the caller. The returned
`LroNacStandardResult` keeps each major product accessible for debugging,
notebooks, and downstream processing. Passing `project=True` adds a `cam2map`
projection stage after SPICE initialization and writes a Moon Equirectangular
map template in the pipeline workspace; `result.projected` then becomes the
final `result.cube`. Pass `map_file=` instead when you need a custom projection
template. Use `result.app_results` to inspect the structured `AppResult` chain
for diagnostics and provenance.

Use `copy_outputs()` on workflow results when RAM-workspace products should be
preserved after the pipeline context exits:

```python
with pyisis.Pipeline(workspace="ram") as pipe:
    result = pipe.lro_nac_standard("M1498479327LE.IMG", preview=True)
    saved = result.copy_outputs("outputs", overwrite=True)

print(saved["cube"])
print(saved["preview"])
```

Individual `CubeHandle` and `ProductHandle` objects also support `copy_to()`
when you are working step by step instead of using a task-level result.

For control-network processing, `Pipeline.control_network_standard()` provides a
similar task-level wrapper around the common overlap, seed, registration,
optional bundle-adjustment, check, and statistics apps:

```python
import pyisis

with pyisis.Pipeline(workspace="ram") as pipe:
    result = pipe.control_network_standard(
        "cubes.lis",
        seed_def="grid.def",
        pointreg_def="pointreg.def",
        seed_kwargs={"networkid": "demo", "pointid": "demo????"},
        bundle=True,
        jigsaw_kwargs={"radius": True},
    )

    print(result.overlaps.path)
    print(result.seed_network.path)
    print(result.registered_network.path)
    print(result.network.path)
    print(result.check_report)

    print("control-network AppResult parameters:")
    for app_result in result.network.results:
        print(app_result.app, app_result.parameters)
```

The workflow intentionally keeps the required `.def` files explicit. ISIS
control-network behavior is highly mission and project dependent, so the helper
handles orchestration and RAM-backed intermediate paths while leaving algorithm
settings under the caller's control. Each `result.network.results` entry is an
`AppResult` for one app call, including the resolved input/output paths and
`app_result.parameters` used for that stage.

If you already have a control network and only need diagnostics, use
`Pipeline.control_network_diagnostics()` to run `cnetstats` and `cnetcheck` with
the required cnetcheck output prefix managed inside a pipeline workspace:

```python
import pyisis

with pyisis.Pipeline(workspace="ram", keep=True) as pipe:
    result = pipe.control_network_diagnostics(
        "cubes.lis",
        "registered.net",
        prefix=pipe.workspace_path / "cnetcheck",
        check_kwargs={"lowcoverage": False},
    )

    print(result.stats_log)
    print(result.check_report)
    print(sorted(path.name for path in result.check_prefix.path.iterdir()))
```

For the optional three-image seed/register integration test, prepare a combined
ISISDATA view that reuses an existing `base` tree and a minimal cached MGS tree:

```bash
python scripts/prepare_three_image_control_isisdata.py \
  --base-isisdata /mnt/e/ISISDATA \
  --mgs-cache /tmp/pyisis-mgs-min \
  --output /tmp/pyisis-three-image-control-isisdata \
  --skip-download \
  --force

export PYISIS_THREE_IMAGE_POINTREG_ISISDATA=/tmp/pyisis-three-image-control-isisdata
python -m unittest \
  tests.unitTest.isis_applications_unit_test.IsisApplicationsUnitTest.test_three_image_control_network_seed_register_success_does_not_exit_python_process \
  -v
```

If the MGS cache has not been prepared yet, rerun the preparation command with
`--download`. The helper intentionally requests only the small MGS text kernels
needed by the minimized test labels and leaves the large CK/SPK files out of the
download command. The generated combined directory contains links to the real
data trees; no mission data is copied into the repository.

### Option B: build from source and install into the current Python environment

This is the **recommended** and most reliable installation method at the moment.

If you want the shortest repo-native entrypoint for the standard build + test + smoke flow, prefer:

```bash
scripts/build_test_smoke.sh full
```

1. Activate the ISIS conda environment you have already prepared.
2. Use that environment as both:
   - the Python interpreter source
   - the ISIS headers / libraries source
3. Configure and build this repository.
4. Install it into the current environment's `site-packages` via `cmake --install`.

A standard workflow looks like this:

```bash
export ISIS_PREFIX="$CONDA_PREFIX"
cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DPython3_EXECUTABLE="$CONDA_PREFIX/bin/python" \
  -DISIS_PREFIX="$ISIS_PREFIX"
cmake --build build -j"$(nproc)"
cmake --install build
```

If you want to exclude ASP / VW camera libraries from the link step during configuration, enable the CMake option below:

```bash
cmake -S . -B build \
   -DCMAKE_BUILD_TYPE=Release \
   -DPython3_EXECUTABLE="$CONDA_PREFIX/bin/python" \
   -DISIS_PREFIX="$ISIS_PREFIX" \
   -DISIS_EXCLUDE_ASP_VW_CAMERA_LIBS=ON
```

When `-DISIS_EXCLUDE_ASP_VW_CAMERA_LIBS=ON` is enabled, the build will exclude `libAsp*` and `libVw*` from the extra ISIS camera library link set while keeping the default behavior unchanged when the option is omitted or set to `OFF`.

After installation, `isis_pybind` will be copied into the current Python environment's `site-packages`.

### Option C: temporary use directly from the build tree

If you only want to develop, debug, or do a quick trial run, you can skip installation and use the package directly from the build tree:

```bash
export PYTHONPATH="$PWD/build/python${PYTHONPATH:+:$PYTHONPATH}"
python -c "import isis_pybind; print(isis_pybind.__file__)"
```

This is suitable for:

- local development
- quick smoke tests
- temporary validation of examples or unit tests

But it is not the recommended formal installation method for end users.

## Installing the shared library: what actually matters

In this project, the “generated binding shared library” is `isis_pybind/_isis_core*.so`.

### Recommended installation method

Prefer:

```bash
cmake --install build
```

Why:

- it installs `__init__.py` and `_isis_core*.so` together in the correct location
- it also includes `LICENSE`
- it avoids the easy-to-make situation where “the `.so` exists, but the Python package structure is incomplete”

### Manual installation method

If you download a **prebuilt binary artifact** from a GitHub Release, and that artifact already contains a complete package directory such as:

```text
isis_pybind/
  __init__.py
  _isis_core.cpython-312-x86_64-linux-gnu.so
  LICENSE
```

then you can copy the entire `isis_pybind/` directory into the target Python environment's `site-packages` directory.

For a conda environment on Linux, the destination is typically:

```text
$CONDA_PREFIX/lib/pythonX.Y/site-packages/isis_pybind/
```

For example, if your target environment is a conda ISIS environment using CPython 3.12, the final path often looks like:

```text
/home/your_user/miniconda3/envs/your_env_name/lib/python3.12/site-packages/isis_pybind/
```

If you are copying from a local build of this repository, prefer copying the fully built package directory:

```text
build/python/isis_pybind/
```

instead of copying only the source-side directory:

```text
python/isis_pybind/
```

because the built package directory includes the compiled extension module `_isis_core*.so` together with `__init__.py`.

You can ask the target environment itself for the correct `site-packages` path with:

```bash
python -c "import sysconfig; print(sysconfig.get_path('purelib'))"
```

Then copy the entire built package directory into that location so that the result becomes:

```text
<site-packages>/isis_pybind/__init__.py
<site-packages>/isis_pybind/_isis_core.cpython-312-x86_64-linux-gnu.so
<site-packages>/isis_pybind/LICENSE
```

Make sure the target environment uses a compatible Python ABI. For example, a file named `_isis_core.cpython-312-x86_64-linux-gnu.so` is built for CPython 3.12 and should be installed into a Python 3.12 environment rather than copied into Python 3.11 or 3.13.

> Copying only `_isis_core*.so` by itself is not recommended.

### Shared-library loading requirements

For the split Linux pip wheels, runtime loading is bootstrapped from the installed `usgs-pyisis-runtime-linux-x86-64-*` shard packages. For source builds or manually copied artifacts, the target machine must still resolve external dependencies such as:

- `libisis.so`
- Qt shared libraries
- required camera / projection / Bullet-related libraries

Therefore, **do not copy `_isis_core*.so` by itself**; install the main wheel together with the matching runtime wheel, or build/install inside a compatible ISIS environment.

## How to verify the installation

At minimum, perform these three checks.

For repository-local validation after code changes, you can also start with:

```bash
scripts/build_test_smoke.sh full
```

### 1. Verify that Python can import the package

```bash
python -c "import isis_pybind as ip; print(ip.__file__)"
```

### 2. Verify that the core extension is loaded

```bash
python -c "import isis_pybind as ip; print(hasattr(ip, 'Cube'), hasattr(ip, 'Camera'))"
```

### 3. Run the minimal smoke flow

```bash
python tests/smoke_import.py
```

If all three pass, that generally means:

- the `isis_pybind` package path is correct
- `_isis_core` can be loaded by Python
- the basic runtime dependencies are available

## Example: forward intersection

The repository already includes a ready-to-reference example:

- `examples/forward_intersection.py`
- usage guide: `examples/forward_intersection_usage.md`

This example demonstrates how to:

- open two ISIS cubes
- provide a left-image point
- automatically estimate / match the conjugate point in the right image
- call `Stereo.elevation(...)` to perform forward intersection

Example command using the repository's bundled test data:

```bash
python examples/forward_intersection.py \
  tests/data/mosrange/EN0108828322M_iof.cub \
  tests/data/mosrange/EN0108828327M_iof.cub \
  64.0 \
  512.0
```

If you want to run the example directly from the build tree, make sure Python can see the package under `build/python`, or install it first with `cmake --install build`.

## Example: DOM matching ControlNet workflow

The repository also contains a DOM-to-ControlNet example workflow under:

- `examples/controlnet_construct/`
- end-to-end usage walkthrough: `examples/controlnet_construct/usage.md`
- detailed requirements / workflow notes: `examples/controlnet_construct/requirements_dom_matching_controlnet.md`
- example config: `examples/controlnet_construct/controlnet_config.example.json`

This workflow is intended for the common planetary-photogrammetry pattern:

1. match tie points on orthorectified DOMs,
2. convert DOM-space keypoints back into original-image coordinates,
3. write a pairwise ISIS `ControlNet`,
4. later merge many pairwise `.net` files with `cnetmerge`.

If you want to run the full pipeline step by step as `image_overlap.py` → `examples/image_match/image_match.py` → `controlnet_stereopair.py from-dom-batch` → `controlnet_merge.py`, start with `examples/controlnet_construct/usage.md`.

For the DOM matching stage, it is usually better to set the main `ImageMatch` options explicitly instead of relying on the raw defaults. A practical starting point is:

```json
"ImageMatch": {
   "band": 1,
   "max_image_dimension": 3000,
   "sub_block_size_x": 1024,
   "sub_block_size_y": 1024,
   "overlap_size_x": 128,
   "overlap_size_y": 128,
   "minimum_value": null,
   "maximum_value": null,
   "lower_percent": 0.5,
   "upper_percent": 99.5,
   "invalid_values": [],
   "special_pixel_abs_threshold": 1e300,
   "min_valid_pixels": 64,
   "valid_pixel_percent_threshold": 0.05,
   "ratio_test": 0.75,
   "max_features": null,
   "sift_octave_layers": 3,
   "sift_contrast_threshold": 0.04,
   "sift_edge_threshold": 10.0,
   "sift_sigma": 1.6,
   "crop_expand_pixels": 100,
   "min_overlap_size": 16,
   "use_parallel_cpu": true,
   "num_worker_parallel_cpu": 8,
   "write_match_visualization": true,
   "match_visualization_scale": 0.3333333333333333
}
```

Here:

- `valid_pixel_percent_threshold = 0.05` skips any tile whose valid-pixel ratio is below $5\%$;
- `num_worker_parallel_cpu = 8` starts the CPU process-pool worker cap at a conservative but practical value, while the actual runtime worker count still contracts automatically to the tile count when needed.

The sample config at `examples/controlnet_construct/controlnet_config.example.json` now includes those recommendations, and both `examples/controlnet_construct/run_pipeline_example.sh` and `examples/controlnet_construct/run_image_match_batch_example.sh` will forward the `ImageMatch` section into `examples/image_match/image_match.py` as default matching parameters. The shared `image_match.py` entrypoint itself also supports `--config`, so if you call it directly you can use the same config file instead of spelling every parameter out on the command line.

For example:

```bash
python examples/image_match/image_match.py \
  --config examples/controlnet_construct/controlnet_config.example.json \
  left_dom.cub right_dom.cub left.key right.key
```

If you also pass an explicit CLI option such as `--ratio-test 0.8` or `--num-worker-parallel-cpu 4`, the CLI value still overrides the config default.

If you want a copy-ready batch template instead of assembling the parameters yourself, `examples/controlnet_construct/usage.md` now includes a more visible “recommended parameter template” section with:

- a ready-to-run `run_pipeline_example.sh` template,
- a manual batch `examples/image_match/image_match.py` template,
- quick tuning guidance for `0.05`, `0.03`, and `0.1`.

If you prefer a shorter standalone entry point, you can now also jump directly to:

- `examples/controlnet_construct/recommended_batch_templates.md`
- `examples/controlnet_construct/run_image_match_batch_example.sh`

From the current workflow revision onward, the example wrapper scripts also document and use these defaults more explicitly:

- CPU tiled matching is enabled by default unless you pass `--no-parallel-cpu`;
- `run_image_match_batch_example.sh` keeps stdout compact by default and treats `work/match_metadata/` as the primary per-pair diagnostics sink;
- `run_image_match_batch_example.sh` writes **pre-RANSAC** match previews into `work/match_viz/` by default;
- `run_pipeline_example.sh` writes both:
   - **pre-RANSAC** previews into `work/match_viz/`, and
   - **post-RANSAC** previews into `work/match_viz_post_ransac/`.

If you want to disable the pre-RANSAC previews when calling the batch image-match wrapper, forward `-- --no-write-match-visualization` to `examples/image_match/image_match.py`.

The output-style convention is now intentionally consistent across the example wrappers: keep terminal output compact, and prefer JSON files for detailed diagnostics. In practice that means:

- `run_image_match_batch_example.sh` mainly prints batch progress on stdout, while per-pair diagnostics live in `work/match_metadata/`;
- `run_pipeline_example.sh` prints step summaries on stdout, while stage JSON summaries live in `work/reports/` and `work/match_results/`;
- if you need the full `examples/image_match/image_match.py` result payload itself, call it directly or forward its own `--result-output` option through the wrapper.

The current implementation split is now real rather than wrapper-only: `examples/image_match/` is the shared source-of-truth for DOM matching and DOM-preparation logic, while `examples/controlnet_construct/image_match.py` and `examples/controlnet_construct/dom_prepare.py` remain as compatibility wrappers for older scripts and imports.

### Single stereo pair

If you already have DOM-space `.key` files for one stereo pair, you can build a pairwise ControlNet like this:

```bash
python examples/controlnet_construct/controlnet_stereopair.py from-dom \
   left_pair_A.key \
   left_pair_B.key \
   left_dom.cub \
   right_dom.cub \
   left_original.cub \
   right_original.cub \
   examples/controlnet_construct/controlnet_config.example.json \
   pair_outputs/left__right.net \
   --pair-id S1 \
   --report-path pair_outputs/left__right.summary.json
```

Notes:

- `PointIdPrefix` comes from the config JSON.
- `--pair-id S1` adds a pair-specific namespace such as `P_S1_00000001`, which helps avoid accidental `PointId` collisions when multiple pairwise nets are later merged with `cnetmerge`.
- If you omit `--pair-id`, the script falls back to the config's optional `PairId`; if neither is set, it keeps the backward-compatible `P00000001`-style behavior.

### Batch mode across `images_overlap.lis`

If you already produced DOM-space key files for every overlap pair listed in `images_overlap.lis`, you can batch-build all pairwise ControlNets with automatic stereo-pair IDs:

```bash
python examples/controlnet_construct/controlnet_stereopair.py from-dom-batch \
   work/images_overlap.lis \
   work/original_images.lis \
   work/doms_scaled.lis \
   work/dom_keys \
   examples/controlnet_construct/controlnet_config.example.json \
   work/pair_nets \
   --report-dir work/reports \
   --pair-id-prefix S \
   --pair-id-start 1
```

In this mode:

- the script reads `images_overlap.lis` and processes every stereo pair in order;
- it expects per-pair DOM key files inside `work/dom_keys/` using names like `A__B_A.key` and `A__B_B.key`;
- it automatically assigns `S1`, `S2`, `S3`, ... to successive pairs, so users do not need to pass `--pair-id` manually for each pair;
- pairwise `.net` files are written into `work/pair_nets/`;
- per-pair JSON sidecars and the batch summary JSON are written into `work/reports/`.

The resulting per-pair reports record the generated `pair_id`, `point_id_namespace`, and a sample point ID so downstream `cnetmerge` debugging is less of a treasure hunt.

## Unit tests: also useful as usage references

The tests in this repository are both regression checks and practical API usage references.

Key entry points include:

- `tests/smoke_import.py`: quick smoke validation
- `tests/unitTest/_unit_test_support.py`: shared test helpers and environment bootstrap logic
- `tests/unitTest/forward_intersection_example_test.py`: focused regression coverage for the forward-intersection example
- `tests/unitTest/`: detailed usage examples organized by class / module

### Run the full unit test suite

```bash
python -m unittest discover -s tests/unitTest -p "*_unit_test.py"
```

### Run the example-related test

```bash
python -m unittest tests.unitTest.forward_intersection_example_test
```

### Run via CTest

If you have already configured the project with CMake, you can also run:

```bash
ctest --output-on-failure -R python-unit-tests
```

## Release recommendations

A GitHub Release should ideally contain at least the following assets:

1. **Source package**
   - The repository source archive (`zip` / `tar.gz`) generated by GitHub is sufficient.
2. **Linux build artifact**
   - For example: `isis_pybind-linux-x86_64-cp312-isis9.0.0.tar.gz`
   - It should contain the full `isis_pybind/` package directory, not just a bare `.so` file.
3. **Installation instructions**
   - These can live in this README, on the Release page, or in a separate `INSTALL.md`.
4. **Version compatibility notes**
   - A version matrix for Linux / Python / ISIS.
5. **Checksum information**
   - `SHA256SUMS.txt`

### Recommended Release artifact naming

Include the following key information in the asset name:

- platform: `linux-x86_64`
- Python ABI: `cp312`
- ISIS version: `isis9.0.0`
- project version: for example `v1.2.0`

For example:

```text
isis_pybind-v1.2.0-linux-x86_64-cp312-isis9.0.0.tar.gz
SHA256SUMS.txt
```

## Checksum recommendations

When publishing binary artifacts, it is recommended to upload a checksum file as well:

```text
SHA256SUMS.txt
```

After downloading, users can run:

```bash
sha256sum -c SHA256SUMS.txt
```

This helps confirm that:

- the artifact is complete and not corrupted
- the download was not truncated
- the user received the exact build artifact you published

## Publishing to PyPI

For the official PyPI release, publish all Linux x86_64 runtime shard wheels before publishing the main `usgs-pyisis` wheel. The main wheel depends on the runtime packages, so users cannot complete `pip install usgs-pyisis` until these distributions exist on the same package index.

Expected upload set:

- `usgs-pyisis-runtime-linux-x86-64-core`
- `usgs-pyisis-runtime-linux-x86-64-libs-1`
- `usgs-pyisis-runtime-linux-x86-64-libs-2`
- `usgs-pyisis`

Use the official PyPI repository, not TestPyPI:

First refresh the main wheel from the current checkout so the release candidate
does not reuse a stale Python API layer:

```bash
python tools/build_main_wheel.py \
  --version 1.2.9 \
  --isis-prefix /home/xjw/isis/ISIS3-9.0.0-linux-prefix \
  --output-dir dist/main-wheel-rebuild \
  --build-dir .skbuild-main-wheel \
  --release-directory dist/release-candidate-20260627
```

`tools/build_main_wheel.py` cleans its output and build directories by default,
builds the `linux_x86_64` main wheel with structured `CMAKE_ARGS`, retags it to
`manylinux_2_34_x86_64`, verifies both wheel variants with
`tools/verify_main_wheel.py`, and copies them into the release-candidate
directory. Pass `--no-clean` only when you intentionally want to reuse an
existing scikit-build directory. Runtime shard wheels are not rebuilt by this
command.

The recommended safe release-preparation entrypoint is:

```bash
python tools/prepare_pypi_release.py dist/release-candidate-20260627 \
  --version 1.2.9 \
  --force-checksums \
  --local-full-smoke \
  --smoke-map-pixel-resolution 100.0 \
  --repository pypi
```

By default, `tools/prepare_pypi_release.py` verifies the release candidate,
writes or checks `SHA256SUMS.txt`, runs the selected smoke gates, and prints the
exact `twine upload` command for the four upload wheels. It does not upload
anything unless `--upload` is also passed. After the official PyPI token is
configured and the printed command looks right, rerun the same command with
`--upload`.

```bash
python -m twine check dist/release-candidate-20260627/*manylinux_2_34_x86_64.whl
python -m auditwheel show dist/release-candidate-20260627/usgs_pyisis-*-linux_x86_64.whl
python tools/retag_main_wheel.py dist/release-candidate-20260627/usgs_pyisis-*-linux_x86_64.whl --platform-tag manylinux_2_34_x86_64
python tools/verify_release_candidate.py dist/release-candidate-20260627 --version 1.2.9 --write-checksums --force-checksums
python tools/list_release_upload_wheels.py dist/release-candidate-20260627 --version 1.2.9 --twine-command
```

For the full pre-upload gate, also validate installation from a clean venv and run the installed-package smoke script:

To see the installed-package smoke choices before running a data-heavy gate, use
`python scripts/pip_smoke_pipeline.py --list-gates`. It prints the recommended
commands for `lightweight installed smoke`, `real-data preflight`,
`full real workflow`, and `notebook smoke`. Use
`python scripts/pip_smoke_pipeline.py --list-gates --json` when CI, notebooks,
or release dashboards need the same menu as structured data.
The lightweight installed smoke includes a
`pyisis-doctor install check smoke: ok` marker after
`pyisis doctor --install-check --json` verifies the installed runtime, app
bindings, and workflow app coverage. It also includes a
`pyisis doctor --missing-path '$lro/kernels/ck/pyisis-smoke-missing.bc' --json`
missing-path repair plan smoke, so release logs prove the installed CLI exposes
`diagnostic.repair_plan`.
It also runs
`pyisis doctor --isis-output 'Spice file does not exist [$lro/kernels/ck/pyisis-smoke-output-missing.bc]' --json`
as an ISIS-output repair plan smoke, proving pasted stderr or `print.prt`
snippets produce `"kind": "pyisis-output-diagnostic"` with the same
repair-plan shape.
The same installed smoke also runs the dedicated ISISDATA command group:
`pyisis data doctor`, `pyisis data missing-path`,
`pyisis data diagnose-output`, and `pyisis data repair-script`.
`tools/verify_release_candidate.py` requires the captured smoke output to
include these five lightweight smoke markers before an official upload:

```text
pyisis-doctor install check smoke: ok
pyisis data doctor smoke: ok
pyisis data missing-path smoke: ok
pyisis data diagnose-output smoke: ok
pyisis data repair-script smoke: ok
```
The verifier captures and audits this output even when
`--smoke-full-real-workflow` is not enabled.

```bash
python tools/verify_release_candidate.py dist/release-candidate-20260627 \
  --version 1.2.9 \
  --write-checksums \
  --force-checksums \
  --local-full-smoke \
  --smoke-map-pixel-resolution 100.0
```

Only upload the four paths printed by `tools/list_release_upload_wheels.py`. The release-candidate directory can also contain `linux_x86_64` intermediate wheels used for retagging; those are not the upload artifacts. `tools/verify_release_candidate.py` verifies the upload set, main wheel metadata/API markers, `twine check`, and `SHA256SUMS.txt` for those same four upload wheels only. `--local-full-smoke` is the local release preset for this workstation: it enables `--pip-smoke`, uses `/mnt/e/ISISDATA` and `/mnt/e/testData/eq/M1498479327LE.IMG` by default, includes `--smoke-full-real-workflow`, and runs notebook smoke with `--smoke-notebooks`. You can still override the default data paths with explicit `--smoke-isisdata` and `--smoke-img` values. With `--pip-smoke`, the verifier creates a fresh virtual environment, installs `usgs-pyisis` from the release directory with `--no-index --find-links`, runs `scripts/pip_smoke_pipeline.py` against the installed package, and verifies that installed `pyisis` reports mission-aware ISISDATA diagnostics for missing SPICE kernels. Including `--smoke-full-real-workflow` runs the installed real LRO NAC smoke from IMG import through `spiceinit`, `footprintinit`, `cam2map`, and PNG preview. For finer-grained debugging, `--smoke-real-workflow --smoke-spice --smoke-footprint --smoke-project` reaches the same projection stage step by step; partial real workflow smoke validates the requested app chain and prints `real workflow apps:` plus `real workflow payload validation: ok` for the exact stages requested. `--smoke-map-pixel-resolution` controls the generated Moon Equirectangular map template. Including `--smoke-notebooks` runs `scripts/execute_pyisis_notebook_smoke.py` against notebook cells tagged `pyisis-smoke`, so release checks also exercise safe notebook examples without running data-heavy cells.

The full real workflow gate also performs `full real workflow payload validation`:
it checks `app_results`, the ordered app chain, key app parameters, and the
`spiceinit -> footprintinit -> cam2map` segment before printing the structured
workflow payload. `tools/verify_release_candidate.py` requires the installed
smoke output to include `full real workflow payload validation: ok`; otherwise
the release gate fails. When running `scripts/pip_smoke_pipeline.py` directly,
add `--report-json outputs/pip_smoke_full_real_workflow_report.json` to keep a
`pyisis-pip-smoke-report` artifact with inputs, app sequence, output paths,
the structured workflow payload, and cleanup status. The release verifier also
checks the reported `projected` and `preview` products prove `exists=True` and
`size > 0`, so a report with only placeholder paths is not accepted.
The same installed smoke command also keeps `--control-network-dry-run` enabled
by default; `tools/verify_release_candidate.py` requires the output markers
`control-network AppResult provenance: ok` and `control-network dry run: ok`
so the release gate proves the control-network wrapper preserved app
parameters and completed the dry-run app chain.

Store the official PyPI token in `~/.pypirc` or pass it through `TWINE_USERNAME=__token__` and `TWINE_PASSWORD`. Do not reuse a TestPyPI token for the official upload.

The main `usgs-pyisis` wheel intentionally depends on the runtime shard wheels for ISIS and Qt shared libraries. Do not use `auditwheel repair` to vendor those large libraries into the main wheel unless the packaging model changes; verify the compatible platform tag with `auditwheel show`, retag the main wheel with `tools/retag_main_wheel.py`, then run `tools/verify_main_wheel.py` to check the final wheel's version, platform tag, required `pyisis` high-level API markers, and runtime shard dependencies.

## Common issues

### 1. `import isis_pybind` fails, or `_isis_core` is missing

Check these first:

- whether the current Python is **CPython 3.12**
- whether `isis_pybind` is coming from the intended build or install environment
- whether an old `build/python` artifact is being picked up accidentally

### 2. `libisis.so` or another shared library cannot be found

This usually means:

- one of the matching split runtime shard wheels was not installed
- or, for source builds, the current shell / Python runtime is not pointing to the correct ISIS environment

### 3. Examples or tests complain about `ISISDATA`

- For real workflows, configure a complete `ISISDATA`
- Repository tests will usually try `tests/data/isisdata/mockup/` automatically
- But not every real-world workflow can rely on mock data

### 4. Can this be supported as a normal `pip install` package?

For Linux x86_64 binary wheels, yes: `pip install usgs-pyisis` is designed to pull the matching runtime shard wheels automatically.

The important limitation is that this is not a pure-Python package. The pip model requires publishing all of these distributions:

- `usgs-pyisis`
- `usgs-pyisis-runtime-linux-x86-64-core`
- `usgs-pyisis-runtime-linux-x86-64-libs-1`
- `usgs-pyisis-runtime-linux-x86-64-libs-2`

Source distributions and developer builds still require:

- an external ISIS SDK/runtime
- Qt and other C++ shared libraries at build time
- a compatible CPython ABI

## License

The binding-layer code and Python entry-point code authored in this repository are distributed under the:

- `MIT License`

See:

- `LICENSE`

Upstream ISIS source code, third-party dependencies, and external shared libraries remain under their respective licenses.
