Skip to content
Merged

Gpu #29

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@ jobs:
python-version: ["3.10", "3.12"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Cache pip packages
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Cache Lightkurve data
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.lightkurve/cache/mastDownload
key: ${{ runner.os }}-lightkurve-${{ hashFiles('pyproject.toml') }}
Expand All @@ -51,7 +51,7 @@ jobs:
pip install -e ".[dev,gui]"

- name: Run tests
run: pytest -v -m "not notebook" -n auto
run: pytest -v -m "not notebook and not network" -n auto

- name: Verify package builds cleanly
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"

Expand Down
87 changes: 60 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,52 @@ Although `quicklook` is optimized to find transiting exoplanets, it can also det

## Installation

Requires Python 3.10+. Install with `uv` (recommended) or `pip`:
Requires Python 3.10+. Choose either `uv` (recommended) or `pip` below.

<details open>
<summary><strong>uv (recommended)</strong></summary>

```bash
# uv (recommended)
# Command-line application
uv tool install quicklook-package

# or pip
pip install -U quicklook-package
# Or install this repository, including development dependencies
uv sync --extra dev

# Optional extras for a repository installation
uv sync --extra gui # Web GUI
uv sync --extra gpu # GPU transit search (CUDA 12)
uv sync --extra notebooks # Jupyter notebooks
```

### Optional extras
Run commands from a repository installation with `uv run`, for example
`uv run quicklook --help`.

```bash
# Web GUI
pip install -U "quicklook-package[gui]"
</details>

# Jupyter notebooks
pip install -U "quicklook-package[notebooks]"
<details>
<summary><strong>pip</strong></summary>

# Development (testing, linting, formatting)
pip install -U "quicklook-package[dev]"
```bash
# Command-line application
python -m pip install -U quicklook-package

# Optional extras
python -m pip install -U "quicklook-package[gui]" # Web GUI
python -m pip install -U "quicklook-package[gpu]" # GPU transit search (CUDA 12)
python -m pip install -U "quicklook-package[notebooks]" # Jupyter notebooks
python -m pip install -U "quicklook-package[dev]" # Development tools
```

Once installed, run commands directly, for example `quicklook --help` or
`ql --help`.

</details>

With the `gpu` extra installed, QuickLook uses GTLS when a CUDA device is
visible and automatically falls back to the standard CPU TLS implementation
when GTLS or a GPU is unavailable, or when GPU execution fails to initialize.

## Try it on Google Colab

<a href="https://colab.research.google.com/github/jpdeleon/quicklook/blob/main/notebook/examples.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>
Expand All @@ -57,15 +80,20 @@ pip install -U "quicklook-package[dev]"

### Command line

The `ql` CLI provides subcommands for analysis and post-processing:
A single `quicklook` command groups every subcommand (the shorter `ql` alias
is equivalent):

```bash
ql --help # show commands: run, read-tls, rank-tls
ql run --help # full analysis options
ql read-tls --help # extract TLS results to CSV
ql rank-tls --help # filter and rank candidates
uv run quicklook --help # show commands: run, read-tls, rank-tls, gui
uv run quicklook run --help # full analysis options
uv run quicklook read-tls --help # extract TLS results to CSV
uv run quicklook rank-tls --help # filter and rank candidates
uv run quicklook gui --help # launch the web GUI
```

Drop the `uv run` prefix once the package is installed on your `PATH`
(e.g. `quicklook run ...` or `ql run ...`).

```bash
# Basic run on the latest TESS sector
ql run --name WASP-21 --save --verbose
Expand Down Expand Up @@ -128,22 +156,25 @@ locally via effective-PSF (ePSF) photometry.
### Web GUI

```bash
ql-gui
uv run quicklook gui # http://127.0.0.1:5000
uv run quicklook gui --host 0.0.0.0 --port 8080
```

Open http://127.0.0.1:5000 in your browser. Enter a target, adjust parameters, and click **Run QuickLook**. Progress is streamed live via WebSocket. Supports single targets, batch submission, and each-sector mode.

![QuickLook Web GUI](docs/img/ql-gui.png)

The Flask debugger is off by default. Set `QUICKLOOK_DEBUG=1` to enable it and
the auto-reloader while developing:
The Flask debugger is off by default. Pass `--debug` (or set `QUICKLOOK_DEBUG=1`)
to enable it and the auto-reloader while developing:

```bash
QUICKLOOK_DEBUG=1 ql-gui
uv run quicklook gui --debug
QUICKLOOK_DEBUG=1 uv run quicklook gui
```

Leave it unset on any host other users can reach — the Werkzeug debugger
exposes an interactive console to whoever can open the port.
Leave it off on any host other users can reach — the Werkzeug debugger
exposes an interactive console to whoever can open the port. The standalone
`ql-gui` command remains available as an alias for `quicklook gui`.

## Output figure

Expand All @@ -169,12 +200,14 @@ The 9-panel figure shows:

## CLI tools

All subcommands are available under either `quicklook` or the shorter `ql` alias.

| Command | Description |
|---------|-------------|
| `ql run` | Run the full QuickLook pipeline on a target |
| `ql read-tls` | Extract TLS results from a directory of `.h5` files into a CSV |
| `ql rank-tls` | Filter and rank candidates by SDE from the CSV output |
| `ql-gui` | Launch the web GUI (requires `[gui]` extra) |
| `quicklook run` | Run the full QuickLook pipeline on a target |
| `quicklook read-tls` | Extract TLS results from a directory of `.h5` files into a CSV |
| `quicklook rank-tls` | Filter and rank candidates by SDE from the CSV output |
| `quicklook gui` | Launch the web GUI (requires `[gui]` extra); also available as `ql-gui` |

## Batch processing

Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,17 @@ dev = [

gui = ["flask", "flask-sock", "pytest-flask"]

gpu = ["gputls", "cupy-cuda12x[ctk]"]

notebooks = [
"ipykernel",
"jupyter",
"nbconvert"
]

[tool.uv.sources]
gputls = { git = "https://github.com/jpdeleon/GTLS.git" }

[project.urls]
Homepage = "https://github.com/jpdeleon/quicklook"

Expand Down Expand Up @@ -97,7 +102,8 @@ ignore = [
]

[project.scripts]
ql = "quicklook.cli.ql:main"
quicklook = "quicklook.cli.app:app"
ql = "quicklook.cli.app:app"
read_tls = "quicklook.cli.read_tls:main"
rank_tls = "quicklook.cli.rank_tls:main"
ql-gui = "quicklook.app.app:main"
55 changes: 48 additions & 7 deletions quicklook/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def __init__(self, fallback):
def set_stream(self, stream):
self._local.stream = stream

def set_fallback(self, stream):
self._fallback = stream

def clear_stream(self):
self._local.stream = None

Expand All @@ -84,8 +87,6 @@ def encoding(self):

_tls_stdout = _ThreadLocalStream(_real_stdout)
_tls_stderr = _ThreadLocalStream(_real_stderr)
sys.stdout = _tls_stdout
sys.stderr = _tls_stderr


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -281,7 +282,12 @@ def parse_sigma(val, default_lo=10, default_hi=5):

# Open the log file and set up streams — file stays open until the
# outermost finally block so no concurrent write hits a closed FD.
log_fh = open(log_file, "w", buffering=1, encoding="utf-8")
# Keep stdout/stderr in append mode too. Loguru uses a separate
# append-mode descriptor below; a normal ``w`` descriptor would keep
# its own offset and overwrite Loguru messages written in between.
log_fh = open(log_file, "a", buffering=1, encoding="utf-8")
log_fh.seek(0)
log_fh.truncate()
_tls_stdout.set_stream(log_fh)
_tls_stderr.set_stream(log_fh)

Expand Down Expand Up @@ -1392,11 +1398,46 @@ def compare():
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def run_gui(host="127.0.0.1", port=5000, debug=None):
"""Launch the Flask development server for the QuickLook GUI.

Parameters
----------
host : str
Interface to bind to (default ``127.0.0.1``, localhost only).
port : int
Port to listen on (default ``5000``).
debug : bool | None
Enable the Werkzeug debugger and auto-reloader. ``None`` (the default)
consults the ``QUICKLOOK_DEBUG`` environment variable. The debugger
exposes an interactive console (arbitrary code execution) to anyone who
can reach the port, so it is opt-in.
"""
if debug is None:
debug = os.environ.get("QUICKLOOK_DEBUG", "").lower() in ("1", "true", "yes")

# Route output only while the server is running. Installing these wrappers
# at module import time captured temporary streams owned by pytest, Typer,
# and other embedders; once those streams closed, later writes failed with
# ``ValueError: I/O operation on closed file``.
previous_stdout = sys.stdout
previous_stderr = sys.stderr
_tls_stdout.set_fallback(previous_stdout)
_tls_stderr.set_fallback(previous_stderr)
sys.stdout = _tls_stdout
sys.stderr = _tls_stderr
try:
app.run(host=host, port=port, debug=debug, threaded=True)
finally:
if sys.stdout is _tls_stdout:
sys.stdout = previous_stdout
if sys.stderr is _tls_stderr:
sys.stderr = previous_stderr


def main():
# The Werkzeug debugger exposes an interactive console (arbitrary code
# execution) to anyone who can reach the port, so it is opt-in.
debug = os.environ.get("QUICKLOOK_DEBUG", "").lower() in ("1", "true", "yes")
app.run(debug=debug, threaded=True)
"""Entry point for the ``ql-gui`` console script."""
run_gui()


if __name__ == "__main__":
Expand Down
26 changes: 26 additions & 0 deletions quicklook/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,5 +600,31 @@ def rank_tls(
typer.echo(f"Copied: {src_path} -> {dst_path}")


@app.command()
def gui(
host: str = typer.Option("127.0.0.1", "--host", help="Interface to bind (default: localhost)"),
port: int = typer.Option(5000, "--port", help="Port to listen on"),
debug: bool = typer.Option(
False, "--debug", help="Enable the Werkzeug debugger and auto-reloader (dev only)"
),
):
"""Launch the QuickLook web GUI (Flask). Needs the optional gui extra.

Open http://<host>:<port> in a browser to run analyses interactively.
The Werkzeug debugger exposes an interactive console, so ``--debug`` is
opt-in; leave it off on any host other users can reach.

Examples:

quicklook gui
quicklook gui --host 0.0.0.0 --port 8080
"""
from quicklook.app.app import run_gui

# An explicit --debug forces the debugger on; without it, defer to the
# QUICKLOOK_DEBUG environment variable handled inside run_gui.
run_gui(host=host, port=port, debug=True if debug else None)


if __name__ == "__main__":
app()
18 changes: 4 additions & 14 deletions quicklook/cli/ql.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env python
"""Target-name sanitization and CLI redirect to the unified Typer app."""
"""Target-name sanitization shared by the CLI and web GUI.

import sys as _sys
The user-facing commands live in :mod:`quicklook.cli.app` (the unified Typer
app exposed as the ``quicklook`` and ``ql`` console scripts).
"""

from quicklook.exceptions import InvalidInputError

Expand Down Expand Up @@ -34,15 +36,3 @@ def sanitize_target_name(name: str) -> str:
if any(char in name for char in ("/", "\\", "\x00")) or ".." in name or name.startswith("."):
raise InvalidInputError(f"Invalid target name: {name!r}")
return name


def main():
"""Redirect to the unified Typer CLI (``ql run``)."""
_sys.argv = [_sys.argv[0], "run"] + _sys.argv[1:]
from quicklook.cli.app import app

app()


if __name__ == "__main__":
main()
Loading
Loading