This file is a merged representation of the entire codebase, combining all repository files into a single document.
Generated by Repomix on: 2025-06-30T14:28:34.609Z

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's
  configuration.
- Binary files are not included in this packed representation. Please refer to
  the Repository Structure section for a complete list of file paths, including
  binary files.

Additional Info:
----------------

================================================================
Directory Structure
================================================================
misfits/
  __init__.py
  app.py
  data.py
  headers.py
  log.py
  logo.py
  misfits.tcss
  mtypes.py
  screens.py
  suggester.py
  utils.py
tests/
  ascii_i4-i20.fits
  ascii.fits
  history_header.fits
  LICENSE.rst
  o4sp040b0_raw.fits
  verify.fits
.gitattributes
.gitignore
conda-env.yml
misfits-vhs.tape
pyproject.toml
README.md

================================================================
Files
================================================================

================
File: misfits/__init__.py
================
import importlib.metadata

__version__ = importlib.metadata.version("misfits")

================
File: misfits/app.py
================
"""
A terminal FITS viewer with interactive tables.

Author: Giuseppe Dilillo
Date:   August 2024
"""

from asyncio import sleep
from asyncio import to_thread
from math import ceil
from pathlib import Path
from typing import Iterable

from astropy.io.fits import FITS_rec
import click
from textual import on
from textual import work
from textual.app import App
from textual.app import ComposeResult
from textual.app import SystemCommand
from textual.containers import Horizontal
from textual.design import ColorSystem
from textual.message import Message
from textual.reactive import reactive
from textual.screen import Screen
from textual.theme import Theme
from textual.widgets import DataTable
from textual.widgets import Footer
from textual.widgets import Input
from textual.widgets import Label
from textual.widgets import Static
from textual.widgets import TabbedContent
from textual.widgets import TabPane
from textual.widgets import Tree
from textual.widgets.tabbed_content import ContentTabs

from misfits.data import _validate_fits
from misfits.data import DataContainer
from misfits.data import get_fits_content
from misfits.headers import AnimatedLabel
from misfits.headers import MainHeader
from misfits.log import log
from misfits.screens import EscapableFileExplorerScreen
from misfits.screens import FileExplorerScreen
from misfits.screens import HeaderEntry
from misfits.screens import InfoScreen
from misfits.screens import LogScreen
from misfits.suggester import PathSuggester
from misfits.utils import catchtime
from misfits.utils import disable_inputs

deepgreen_theme = Theme(
    name="deepgreen",
    primary="#03A062",  # matrix green
    secondary="#03A062",
    warning="#03A062",
    error="#ff0000",
    success="#00ff00",
    accent="#00ff00",
    dark=True,
    variables={
        "block-cursor-text-style": "none",
        "footer-key-foreground": "#88C0D0",
        "input-selection-background": "#81a1c1 35%",
    },
)

# fits table are displayed in small chunks (pages) to achieve better performances.
# this parameter set the number of rows displayed per page within a FitsTable.
PAGE_LEN = 100


class FitsTable(DataTable):
    """Displays fits data as a table arranged in pages."""

    BINDINGS = [
        ("shift+left", "back_page()", "Back"),
        ("shift+right", "next_page()", "Next"),
        ("shift+up", "first_page()", "First"),
        ("shift+down", "last_page()", "Last"),
    ]
    PAGE_DELAY = 1 / 60

    page_no = reactive(1, bindings=True)

    class QuerySucceded(Message):
        """A message to be sent when a query completes."""

        def __init__(self, query_succeded: bool) -> None:
            self.value = query_succeded
            super().__init__()

    def __init__(self, data: DataContainer):
        """
        :param data: a data container, see `misfits.data.DataContainer`.
        """
        super().__init__()
        self.data: DataContainer = data
        self.page_len = PAGE_LEN
        self.mask = None
        self.page_no = 1  # starts from one
        self.page_tot = max(ceil(len(self.data) / self.page_len), 1)
        log.push_data_info(data)

    def on_mount(self):
        self.border_title = "Table"
        self.cursor_type = "cell"
        self.add_columns(*self.data.get_columns())
        self.show_page()

    # runs possibly slow filter operation with a worker to avoid UI lags
    @work(exclusive=True, group="filter_table")
    async def filter_table(self, query: str):
        """
        Filters a table according to a query and shows a table page.

        :param query: the filter query
        """
        # noinspection PyBroadException
        try:
            _ = await to_thread(self.data.query, query)
        except Exception:
            self.post_message(self.QuerySucceded(False))
            return
        self.page_tot = max(ceil(len(self.data) / self.page_len), 1)
        self.show_page()
        self.post_message(self.QuerySucceded(True))
        log.push(
            f"Filtered table by query {repr(query)}, {len(self.data)} matching entries."
        )

    def page_slice(self):
        """Returns a slice comprising entries to be displayed in present page."""
        page = ((self.page_no - 1) * self.page_len, self.page_no * self.page_len)
        return slice(*page)

    def show_page(self):
        """Displays a table page."""
        self.clear(columns=False)
        self.add_rows(rows=self.data.get_rows(self.page_slice()))
        self.border_subtitle = f"page {self.page_no} / {self.page_tot} "

    # the function is on a worker so that, if user keeps page turn pressed
    # page displays won't accumulate in queue, resulting in pages still getting
    # loaded after user releases the button.
    # sleep enforces a maximum turning rate to tot pages per seconds.
    # maybe we can live without that?
    # TODO: make sure `PAGE_DELAY` is needed
    @work(exclusive=True, group="turn_page")
    async def action_next_page(self):
        """Scrolls to next page."""
        await sleep(self.PAGE_DELAY)
        if self.page_no < self.page_tot:
            self.page_no += 1
            self.show_page()

    @work(exclusive=True, group="turn_page")
    async def action_back_page(self):
        """Scrolls to previous page."""
        await sleep(self.PAGE_DELAY)
        if self.page_no > 1:
            self.page_no -= 1
            self.show_page()

    @work(exclusive=True, group="turn_page")
    async def action_last_page(self):
        """Scrolls to last page"""
        await sleep(self.PAGE_DELAY)
        self.page_no = self.page_tot
        self.show_page()

    @work(exclusive=True, group="turn_page")
    async def action_first_page(self):
        """Scrolls to first page"""
        await sleep(self.PAGE_DELAY)
        self.page_no = 1
        self.show_page()

    def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
        """Checks if an action may run or not, if not greys them out in footer."""
        if action in ["first_page", "back_page"] and self.page_no == 1:
            return None
        if action in ["last_page", "next_page"] and self.page_no == self.page_tot:
            return None
        return True

    # TODO: add methods and binding for scrolling to `n` page.


CLEAR_PROMPT_LABEL = "Clear"


class FilterInput(Static):
    """A widget displaying an input prompt for filtering a table"""

    BINDINGS = [
        ("ctrl+n", "clear()", CLEAR_PROMPT_LABEL),
    ]

    class ClearTable(Message):
        """A message to be sent when a query completes."""

        def __init__(self) -> None:
            super().__init__()

    def compose(self) -> ComposeResult:
        with Horizontal():
            yield Label("[dim italic] query: ")
            yield Input(placeholder=f"COL1 > 42 & COL2 == 3")

    def on_mount(self):
        self.border_title = "Filter"

    def action_clear(self):
        self.query_one(Input).value = ""
        self.post_message(self.ClearTable())


class TableDialog(Static):
    """A widget containing the data table and its filter."""

    def __init__(
        self,
        arr: FITS_rec,
        hide_filter: bool = False,
    ):
        """
        :param arr: The fits records data.
        :param hide_filter: Wether if to show the table filter or none. We do not show
        filter for tables which would require huge loading time, such as tables with
        variable length array columns.
        """
        super().__init__()
        self.arr = arr
        self.hide_filter = hide_filter

    def compose(self) -> ComposeResult:
        data = DataContainer(self.arr)
        yield FitsTable(data)
        if data.can_promote:
            yield FilterInput()

    def on_mount(self):
        self.border_title = "Table"

    # async is needed since `filter_table` calls a worker
    @on(Input.Submitted)
    async def maybe_filter_table(self, event: Input.Submitted):
        # noinspection PyAsyncCall
        self.query_one(FitsTable).filter_table(event.value)
        # we prevent bubbling up of the message, which would affect file input prompt
        event.stop()

    @on(FitsTable.QuerySucceded)
    def color_filter_border(self, message: FitsTable.QuerySucceded):
        if message.value:
            self.query_one(FilterInput).remove_class("error")
        else:
            self.query_one(FilterInput).add_class("error")

    @on(FilterInput.ClearTable)
    def reset_fits_table(self, message: FitsTable.QuerySucceded):
        self.query_one(FitsTable).filter_table("")


class EmptyDialog(Static):
    """A placeholder widget for when an HDU contains images or no data."""

    # TODO: Add a separate placeholder for images.

    def compose(self) -> ComposeResult:
        yield Label("No tables to show")

    def on_mount(self):
        self.border_title = "Table"


class HeaderDialog(Tree):
    """Displays a FITS header as a tree."""

    BINDINGS = [
        ("ctrl+s", "colexp_all", "Collapse/Expand all"),
    ]

    def __init__(self, header: dict, ellipsis: int = 14):
        """
        :param header:
        :param ellipsis: sets length after which apply an ellipsis.
        """
        super().__init__(label="root")
        self.leafs = []
        for key, value in header.items():
            node = self.root.add(label=key)
            label = (
                vstr
                if len(vstr := str(value).strip()) < ellipsis
                else vstr[:ellipsis] + ".."
            )
            leaf = node.add_leaf(label, data=str(value))
            self.leafs.append(leaf)

    def on_mount(self):
        self.border_title = "Header"
        self.guide_depth = 3
        self.show_guides = True
        self.root.expand()

    @on(Tree.NodeSelected)
    def display_content_popup(self, event: Tree.NodeSelected):
        """Opens a pop-up when a header entry is selected."""
        if event.node in self.leafs:
            self.app.push_screen(HeaderEntry(event.node.data))

    def action_colexp_all(self):
        """Collaps or expand all header nodes together."""
        if all(node.is_expanded for node in self.root.children):
            for c in self.root.children:
                c.collapse()
        else:  # some of the node is expanded already
            for node in self.root.children:
                if not node.is_expanded:
                    node.expand()


class HDUPane(TabPane):
    """A container for header and table widgets."""

    class FocusedUnpromotableTable(Message):
        """A message to be sent when loading tables we do not fully support."""

        def __init__(self, table_name) -> None:
            self.table_name = table_name
            super().__init__()

    def __init__(self, content: dict, **kwargs):
        self.content = content
        self._name = content["name"] if content["name"].strip() else "HDU"
        self.focused_already = False
        super().__init__(self._name, **kwargs)
        log.push_hdu_info(content)

    def compose(self) -> ComposeResult:
        with Horizontal():
            yield HeaderDialog(self.content["header"])
            if self.content["is_table"]:
                yield TableDialog(self.content["data"])
            else:
                yield EmptyDialog()

    @on(TabPane.Focused)
    def notify(self, _: TabPane.Focused) -> None:
        """This will alert main app to notify we are on a table with limitations."""
        if (
            not self.focused_already
            and self.content["is_table"]
            and not self.query_one(FitsTable).data.can_promote
        ):
            self.post_message(self.FocusedUnpromotableTable(self.content["name"]))
        self.focused_already = True


BROWSE_FILE_LABEL = "Browse files"


class FileInput(Static):
    """A widget showing an input for file paths."""

    BINDINGS = [
        ("ctrl+n", "clear()", CLEAR_PROMPT_LABEL),
    ]

    def compose(self) -> ComposeResult:
        with Horizontal():
            yield Label(f"[dim italic] path: ")
            yield Input(
                placeholder="~/path/to/some/file.fits",
                suggester=PathSuggester(),
            )

    def on_mount(self):
        self.border_title = "File"

    def set_input_value(self, value: str):
        self.query_one(Input).value = value

    def action_clear(self):
        self.query_one(Input).value = ""


class Misfits(App):
    """Misfits, the main app."""

    CSS_PATH = "misfits.tcss"
    SCREENS = {
        "log": LogScreen,
        "file_explorer": FileExplorerScreen,
        "info": InfoScreen,
    }
    BINDINGS = [
        ("ctrl+o", "open_explorer", BROWSE_FILE_LABEL),
        ("ctrl+l", "show_log", "Log"),
        ("ctrl+j", "show_info", "Info"),
    ]

    def __init__(self, filepath: Path | None, root_dir: Path = Path.cwd()) -> None:
        """
        :param filepath: the file to load at startup.
        If `None`, an unescapable file explorer is shown at startup.
        :param root_dir: will set the directory from which file explorers start.
        """
        super().__init__()
        self.filepath = filepath
        self.rootdir = root_dir
        self.fits_content = []
        self.logstack = []

    # `push_screen_wait` requires a worker
    @work
    async def on_mount(self):
        self.register_theme(deepgreen_theme)
        self.theme = "deepgreen"
        if not self.filepath:
            self.filepath = await self.push_screen_wait(
                FileExplorerScreen(self.rootdir)
            )
        self.query_one(FileInput).set_input_value(str(self.filepath))
        # noinspection PyAsyncCall
        self.populate_tabs()

    def compose(self) -> ComposeResult:
        yield MainHeader()
        yield TabbedContent()
        yield FileInput()
        yield Footer()

    def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
        # skips light mode toggle since, at present, it will mess with CSS and headers
        yield from (
            c for c in super().get_system_commands(screen) if c.title != "Light mode"
        )
        yield SystemCommand(
            "Show log",
            "Displays a log of misfits operations.",
            lambda: self.push_screen("log"),
        )
        yield SystemCommand(
            "More informations",
            "Displays information on misfits.",
            lambda: self.push_screen("info"),
        )

    def action_show_log(self):
        self.push_screen("log")

    def action_show_info(self):
        self.push_screen("info")

    # this is to avoid having secondary key bindings to pop up in footer, when the footer
    # is already crowded, e.g., when going through the fits's table records
    def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
        """Checks if an action may run or not, if not greys them out in footer."""
        _, labels, _ = zip(*self.BINDINGS)
        if action in labels and isinstance(self.focused, FitsTable):
            return False
        return True

    # `populate_tabs` requires a worker
    @on(Input.Submitted)
    async def load_file_content(self, event: Input.Submitted):
        """Accepts and checks message from file input prompt."""
        input_path = Path(event.value)
        if not _validate_fits(input_path):
            self.query_one(FileInput).add_class("error")
            return
        self.query_one(FileInput).remove_class("error")
        self.filepath = input_path
        # noinspection PyAsyncCall
        self.populate_tabs()

    @on(HDUPane.FocusedUnpromotableTable)
    def notify_unpromotable(self, message: HDUPane.FocusedUnpromotableTable):
        self.notify(
            f"Table {message.table_name} contains array columns with variable length. "
            f"Unfortunately, these columns cannot be displayed. Filter has been disabled.",
            severity="warning",
            timeout=5,
        )

    # `push_screen_wait` requires a worker
    @work
    async def action_open_explorer(self):
        self.filepath = await self.push_screen_wait(
            EscapableFileExplorerScreen(self.rootdir)
        )
        # noinspection PyAsyncCall
        self.populate_tabs()
        self.query_one(FileInput).set_input_value(str(self.filepath))
        self.query_one(FileInput).remove_class("error")

    # calls CPU-heavy `get_fits_content`, requiring a worker
    # exclusive because otherwise would result in an error everytime we attempt
    # to open a new one while still loading.
    @work(exclusive=True)
    async def populate_tabs(self) -> None:
        """
        Fills the tabs with data read from the FITS' HDUs.
        """
        async with disable_inputs(
            loading=self.query_one(TabbedContent),
            disabled=[self.query_one(FileInput)],
        ):
            with catchtime() as elapsed:
                tabs = self.query_one(TabbedContent)
                await tabs.clear_panes()
                log.push(f"Opening '{self.filepath}'")
                contents = await get_fits_content(self.filepath)
                for i, content in enumerate(contents):
                    await tabs.add_pane(HDUPane(content, id=(tab_id := f"tab-{i}")))
                    # switches to a pane if that pane contains a table
                    if content["is_table"]:
                        self.query_one(TabbedContent).active = tab_id
            log.push(f"Reading FITS file took {elapsed():.3f} s")
        # play little logo animation
        self.query_one(AnimatedLabel).play()


def click_validate_fits(
    ctx: click.Context, param: click.Parameter, filepath: Path
) -> Path:
    """Click callback validator."""
    if filepath.is_file() and not _validate_fits(filepath):
        raise click.FileError(
            f"Invalid input.",
            hint="Please, check misfits `INPUT_PATH` argument "
            "and make sure it points to a FITS file.",
        )
    return filepath


@click.command()
@click.argument(
    "input_path",
    type=click.Path(exists=True, path_type=Path),
    callback=click_validate_fits,
    default=Path("."),
)
def main(input_path: Path):
    """Misfits is an interactive FITs viewer for the terminal."""
    filepath, rootdir = (
        (None, input_path) if input_path.is_dir() else (input_path, Path.cwd())
    )
    Misfits(filepath, rootdir).run(inline=False)


if __name__ == "__main__":
    app = Misfits(None, Path("."))
    app.run()

================
File: misfits/data.py
================
from collections import OrderedDict
from pathlib import Path
import re
import warnings

from astropy.io import fits
from astropy.io.fits.verify import VerifyWarning
from numpy import round
from pandas import DataFrame
from pandas import Index

from misfits.log import log
from misfits.mtypes import ColumnType
from misfits.mtypes import LogLevel


def is_table(hdu: fits.FitsHDU):
    """Check whether and HDU contains table data."""
    return type(hdu) in [fits.TableHDU, fits.BinTableHDU]


def parse_format(tform: str) -> tuple[int, str, str]:
    """
    Based on  Starlink (STIL) Java implementation, see
    `https://github.com/Starlink/starjava/blob/master/fits/src/main/uk/ac/starlink/fits/ColumnReader.java`

    :param tform: a FITS TFORM string
    :return: an integer representing the column length (1 for scalar columns, 2+ for vector columns),
    a string, representing the type of the column values.
    a string, representing additional information used for interpreting vector and
    variable length columns.
    """
    pattern = r"([0-9]*)([LXBIJKAEDCMPQ])(.*)"
    match = re.match(pattern, tform)
    if not match:
        # TODO: Handle this error
        raise ValueError(f"Error parsing TFORM value {tform}")
    scount = match.group(1)
    type_char = match.group(2)
    matchA = match.group(3).strip()
    count = 1 if scount == "" else int(scount)
    return count, type_char, matchA


def get_column_type(tform: str) -> ColumnType:
    """Classifies a column based on its content."""
    count, type_char, matchA = parse_format(tform)
    # "PQ" type char is used for identifying variable len columns.
    if type_char[0] in "PQ":
        return ColumnType.VARLEN
    elif count == 1:
        return ColumnType.SCALAR
    else:
        return ColumnType.VECTOR


async def get_fits_content(fits_path: str | Path) -> tuple[dict]:
    """Retrieves content from a FITS file and stores it in a tuple dict.
    Each tuple's records referes to one FITS HDU.
    Can take some time for large tables"""

    content = []
    with fits.open(fits_path) as hdul:
        # for the relevant astropy documentation, see:
        # https://docs.astropy.org/en/latest/io/fits/usage/verification.html
        with warnings.catch_warnings(
            record=True,
            category=VerifyWarning,
        ) as ws:
            hdul.verify("fix")
            log.push_verification_warning(ws)
        for hdu in hdul:
            content.append(
                {
                    "name": hdu.name,
                    "type": hdu.__class__.__name__,
                    "header": dict(hdu.header) if hdu.header else None,
                    "is_table": (ist := is_table(hdu)),
                    "data": hdu.data if ist else None,
                }
            )
    return tuple(content)


FITS_SIGNATURE = b"SIMPLE  =                    T"


def _validate_fits(filepath: Path) -> bool:
    """Checks if a file is a FITS."""
    # follows the same approach of astropy.
    try:
        with open(filepath, "rb") as file:
            # FITS signature is supposed to be in the first 30 bytes, but to
            # allow reading various invalid files we will check in the first
            # card (80 bytes).
            simple = file.read(80)
    except OSError:
        return False
    match_sig = simple[:29] == FITS_SIGNATURE[:-1] and simple[29:30] in (b"T", b"F")
    return match_sig


class DataContainer:
    """This class manages FITS table data.
    It makes data representable on the user machine, determines which columns to show,
    dispatch table entries and deals with dataset queries, when queries are possible."""

    def __init__(self, records: fits.FITS_rec):
        self.records: fits.FITS_rec | DataFrame = records
        self._len = len(records)
        self.columns = {
            col.name: get_column_type(col.format) for col in records.columns
        }
        self.displayable_columns = [
            colname
            for colname, coltype in self.columns.items()
            if coltype is not ColumnType.VARLEN
        ]
        self.can_promote = all(
            [coltype != ColumnType.VARLEN for coltype in self.columns.values()]
        )
        self.promoted = False
        self.mask: None | Index = None

    def __len__(self):
        """Returns length of possibly filtered dataset."""
        return self._len

    # table gets promoted when first converted to dataframe.
    # this enables the usage of pandas queries. promotion happens at first filter call.
    # a promoted table cannot be demoted.
    def query(self, query: str):
        """Leverage pandas queries to filter dataset""" ""
        if not self.can_promote:
            raise ValueError("Trying to filter an unpromotable table")
        if not self.promoted:
            self._promote()
        filtered_df = self.records.query(query) if query else self.records
        self.mask = filtered_df.index
        self._len = len(filtered_df)

    def get_rows(self, slice):
        """Dispatch rows based on slice."""
        if self.promoted:
            table_slice = self.records.iloc[self.mask[slice]]
        else:
            # this looks eccentric but is faster than list comprehension (WHY?) and
            # has the benefit of having homogenous formatting with promoted table
            table_slice = self._to_pandas(self.records[slice])
        return table_slice.itertuples(index=False)

    def get_columns(self):
        """Dispatch table's displayable columns"""
        return self.displayable_columns

    @staticmethod
    def _maybe_correct_endianess(records: fits.FITS_rec):
        """Convert FITS records to user machine endiannes if they differ."""
        if not records.dtype.isnative:
            records = records.byteswap().view(records.dtype.newbyteorder("="))
        return records

    def _promote(self):
        """Promote a fits records table to a proper dataframe."""
        assert self.can_promote
        self.promoted = True
        self.records = self._to_pandas(self.records)  # Table(self.records).to_pandas()
        self.mask = self.records.index

    def _to_pandas(self, table):
        """Transforms a fits records table into a dataframe"""
        out = OrderedDict()
        for colname in self.displayable_columns:
            column = self._maybe_correct_endianess(table[colname])
            if self.columns[colname] is ColumnType.VECTOR:
                if column.dtype.kind == "f":
                    # this is a workaround to Textual not applying cell formatting
                    # recursively. TODO: improve this logic?
                    column = round(column, 2)
                column = column.tolist()
            out[colname] = column

        df = DataFrame(out, index=None)
        return df

================
File: misfits/headers.py
================
import asyncio

from rich.text import Text
from terminaltexteffects import Color
from terminaltexteffects import Gradient
from terminaltexteffects.effects.effect_binarypath import BinaryPath
from textual import work
from textual.app import ComposeResult
from textual.containers import Horizontal
from textual.widgets import Label
from textual.widgets import Static

from misfits import __version__


class Header(Static):
    def __init__(
        self,
        *,
        left_label: Label | str | None = None,
        mid_label: Label | str | None = None,
        right_label: Label | str | None = None,
    ):
        self.left_label = (
            Label(left_label) if isinstance(left_label, str) else left_label
        )
        self.mid_label = Label(mid_label) if isinstance(mid_label, str) else mid_label
        self.right_label = (
            Label(right_label) if isinstance(right_label, str) else right_label
        )
        super().__init__()

    def compose(self) -> ComposeResult:
        with Horizontal():
            if self.left_label:
                yield self.left_label
            yield Static()
            if self.mid_label:
                yield self.mid_label
            yield Static()
            if self.right_label:
                yield self.right_label


class AnimatedLabel(Static):
    def __init__(self, text: str):
        super().__init__()
        self.text = text
        effect = BinaryPath(self.text)
        effect.effect_config.final_gradient_stops = Color("FFFFFF")
        effect.effect_config.final_gradient_steps = 8
        effect.terminal_config.canvas_height = 1
        effect.terminal_config.canvas_width = len(self.text)
        self.effect = effect

    @work
    async def play(self) -> None:
        for frame in self.effect:
            self.update(Text.from_ansi(frame))
            await asyncio.sleep(0.0)


class MainHeader(Header):
    def __init__(self):
        self.has_run_before = False
        super().__init__(
            left_label=AnimatedLabel(" misfits"),
            right_label=Label(Text.from_markup(f"[italic dim]v.{__version__} ")),
        )

================
File: misfits/log.py
================
from datetime import datetime

from astropy.io.fits.verify import VerifyWarning

from misfits.mtypes import ColumnType
from misfits.mtypes import LogLevel


class Logger:
    def __init__(self):
        self.logstack = []

    def push(self, message: str, level: LogLevel | None = LogLevel.INFO):
        now_str = "[dim cyan]" + datetime.now().strftime("(%H:%M:%S)") + "[/]"
        match level:
            case LogLevel.INFO:
                prefix = f"{now_str} [dim green][INFO][/]: "
            case LogLevel.WARNING:
                prefix = f"{now_str} [dim yellow][WARNING][/]: "
            case LogLevel.ERROR:
                prefix = f"{now_str} [bold red][ERROR][/]: "
            case _:
                prefix = ""
        self.logstack.append(prefix + message)

    def pop(self) -> str | None:
        return self.logstack.pop(0) if self.logstack else None

    def push_hdu_info(self, content):
        # fmt: off
        self.push(f"Found HDU {repr(content['name'])} of type {repr(content['type'])}")
        if content["is_table"]:
            ncols = len(content["data"].columns)
            self.push(f"HDU contains a table with {len(content['data'])} rows and {ncols} columns")
        # fmt: on

    def push_data_info(self, data):
        # fmt: off
        columns = {coltype: [] for coltype in ColumnType}
        for colname, coltype in data.columns.items():
            columns[coltype].append(colname)

        if scalar_columns := columns[ColumnType.SCALAR]:
            self.push(f"Data table contains {len(scalar_columns)} scalar columns: {scalar_columns}")
        if vector_columns := columns[ColumnType.VECTOR]:
            self.push(f"Data table contains {len(vector_columns)} vector columns: {vector_columns}")
        if varlen_columns := columns[ColumnType.VARLEN]:
            self.push(f"Data table contains {len(varlen_columns)} variable len columns: {varlen_columns}", level = LogLevel.WARNING)
        # fmt: on

    def push_verification_warning(self, warnings: list[VerifyWarning]):
        for w in warnings:
            self.push(str(w.message).rstrip(), level=LogLevel.WARNING)


log = Logger()

================
File: misfits/logo.py
================
from random import choice
from string import ascii_letters
from string import digits

_LOGO = """
   0000000     000000               0000000000000   000000000000000     
   0000000    0000000               000000000000      000000000         
   0000000    000000  0000    0000     00000     0000  000000    0000   
   0000000   0000000  000   0000000    000000000 000   00000   00000000 
   00000000 00000000  000  0000    0   0000000   000    00000 00000   0 
   00000000 0000000   000  00000       0000      000    0000  00000     
   000000000000 0000  000   0000000    0000      000     000    00000   
    000 000000  0000  000       00000  0000      000    0000        0000
   0000  0000   000    00 0000000000   0000       00    0000 0000000000 
   0000  0000   000         00000      00               00      0000    
   0000   00    000                                                     
"""


LOGO = "".join([(choice(ascii_letters + digits) if s == "0" else s) for s in _LOGO])

================
File: misfits/misfits.tcss
================
* {
  scrollbar-color: $surface-lighten-3;
  scrollbar-color-hover: $primary 80%;
  scrollbar-color-active: $primary;
  scrollbar-background: $surface-darken-1;
  scrollbar-background-hover: $surface-darken-1;
  scrollbar-background-active: $surface-darken-1;
  scrollbar-size-vertical: 1;
  scrollbar-size-horizontal: 0;

  &:focus {
    scrollbar-color: $surface-lighten-3;
  }
}

DataTable > .datatable--header {
  text-style: bold;
  background: $primary 30%;
  color: $text;
}

DataTable > .datatable--cursor {
  background: $accent 30%;
}

FitsTable {
  height: 1fr;
  background: $background;
  border: round $secondary;
  border-subtitle-align: right;
  & :focus-within {
    border: round $accent;
  }
}

HeaderDialog {
  width: 1fr; /* change HeaderDialog's ellipsis accordingly */
  border: round $secondary;
  background: $background;
  & :focus-within {
    border: round $accent;
  }
}

TableDialog {
  width: 3fr;
}

EmptyDialog {
  width: 3fr;
  height: 1fr;
  border: round $secondary;
  align: center middle;
  hatch: right green 20%;
}

EmptyDialog Label {
  width: auto;
}

TabbedContent {
  margin-top: 1;
  height: 1fr;  /* keeps loading indicator at center */
  background: $background;
}

TabPane {
  padding: 0;
  border: none;
}

Tabs .-active {
  background: transparent;  /* disables block highlighting */
}

HeaderEntry {
  align: center middle;
}

HeaderEntry Container {
  width: 48;
  height: 12;
  border: thick $background;
  background: $surface;
}

HeaderEntry TextArea {
  margin: 1;
  border: none;
}

LogScreen RichLog {
  background: $background;
}

InfoScreen {
  align: center middle;
}

InfoScreen Container {
  width: 76;
  height: 18;
  border: ascii $primary;
  background: $surface;
}

FileExplorerScreen {
  align: center middle;
}

FileExplorerScreen Container {
  align: center middle;
  width: 75%;
  height: 75%;
  padding: 0 1;
  border: thick $background;
  background: $surface;
}

FileExplorerScreen DirectoryTree {
  border: round $secondary;
  background: $background;
  &.error {
    border: round $error;
  }
}

FileInput {
  /* will align prompt of FileInput to prompt of filter */
  height: 3;
  padding: 0 0 0 1;
  background: $background;
  border: round $primary;
  & :focus-within {
    border: round $accent;
  }
  &.error {
    border: round $error;
  }
}

FileInput Input {
  height: 1;
  width: 1fr;
  border: none;
}

FilterInput {
  border: round $secondary;
  height: 3;
  &:focus-within {
    border: round $accent;
  }
  &.error {
    border: round $error;
  }
}

FilterInput Input {
  height: 1;
  width: 1fr;
  border: none;
}

Header {
  height: 1;
  background: $footer-background;
}

Header Static {
  width: 1fr;
}

Header Label {
  width: auto;
}

================
File: misfits/mtypes.py
================
from enum import Enum


class ColumnType(Enum):
    SCALAR = 0
    VECTOR = 1
    VARLEN = 2


class LogLevel(Enum):
    INFO = 0
    WARNING = 1
    ERROR = 2

================
File: misfits/screens.py
================
import asyncio
from pathlib import Path
from typing import Iterable

from rich.text import Text
from textual import work
from textual.app import ComposeResult
from textual.containers import Container
from textual.screen import ModalScreen
from textual.widgets import DirectoryTree
from textual.widgets import Footer
from textual.widgets import Label
from textual.widgets import RichLog
from textual.widgets import Static
from textual.widgets import TextArea

from misfits.data import _validate_fits
from misfits.headers import Header
from misfits.log import log
from misfits.logo import LOGO


class LogScreen(ModalScreen):
    """An alternative screen showing a log"""

    BINDINGS = [("escape", "app.pop_screen", "Return to dashboard")]

    def compose(self) -> ComposeResult:
        yield Header(mid_label="Log")
        yield RichLog(highlight=True, markup=True)
        yield Footer()

    def on_screen_resume(self):
        """When screen is shown, pushes message on the stack to the screen."""
        while line := log.pop():
            self.query_one(RichLog).write(line)


class InfoScreen(ModalScreen):
    """Shows an information screen."""

    BINDINGS = [("escape", "app.pop_screen", "Return to dashboard")]

    @staticmethod
    def get_text():
        return Text.from_markup(
            f"   A FITS table viewer. [italic]~p24[/].\n"
            f"   [dim]https://github.com/peppedilillo - https://gdilillo.com\n",
        )

    def compose(self) -> ComposeResult:
        with Container():
            yield Label(Text(LOGO, style="green bold"))
            yield Static(self.get_text())
        yield Footer()


class FileExplorerScreen(ModalScreen):
    """A pop-up screen showing a file explorer so that the user may choose an
    input navigating the file system.
    To be used at main app's start-up, if no input file is provided.
    For this reason the screen is not escapable without quitting."""

    BINDINGS = [("ctrl+q", "app.quit", "Quit")]

    def __init__(self, rootdir: Path = Path.cwd()):
        super().__init__()
        self.rootdir = rootdir

    def compose(self) -> ComposeResult:
        with Container():
            yield Header(mid_label="Open File")
            yield FilteredDirectoryTree(self.rootdir)
        yield Footer()

    # threaded because validating requires IO which may cause laggish behaviour
    @work(exclusive=True, group="file_check")
    async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected):
        isvalid = await asyncio.to_thread(_validate_fits, event.path)
        if not isvalid:
            self.query_one(DirectoryTree).add_class("error")
            return
        self.query_one(DirectoryTree).remove_class("error")
        # noinspection PyAsyncCall
        self.dismiss(event.path)


class EscapableFileExplorerScreen(FileExplorerScreen):
    """Like `FileExplorer` but with bindings to leave the screen.
    To be used when a file input has already been provided."""

    BINDINGS = [("escape", "app.pop_screen", "Return to dashboard")]


class FilteredDirectoryTree(DirectoryTree):
    """A directory tree widget filtering hidden files."""

    def filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:
        return [path for path in paths if not path.name.startswith(".")]


class HeaderEntry(ModalScreen):
    """Displays header's entries in a pop-up screen. Useful with long entries."""

    BINDINGS = [("escape", "app.pop_screen", "Return to dashboard")]

    def __init__(self, text: str):
        super().__init__()
        self.text = text

    def compose(self) -> ComposeResult:
        with Container():
            yield Header(mid_label="Header entry")
            yield TextArea.code_editor(self.text, read_only=True)
        yield Footer()

================
File: misfits/suggester.py
================
import itertools
from pathlib import Path

from textual.suggester import Suggester


def is_directory_or_fitsfile(path: Path):
    return path.is_dir() or path.suffix == ".fits"


class PathSuggester(Suggester):
    """Suggest either directories of file with `fits` extension.
    This class is based on an original implementation by Will McGugan, https://github.com/willmcgugan.
    The code was not released as part of a package but provided via private communication.
    It comes with no license.
    Thank you very much for letting me use this, Will!
    TODO: remove once the suggester will be released with textual.
    """

    def __init__(self):
        super().__init__(case_sensitive=True)

    async def get_suggestion(self, value: str) -> str | None:
        """Suggest the first matching directory"""
        try:
            path = Path(value)
            if is_directory_or_fitsfile(path):
                return None
            name = path.name
            possible_paths = [
                str(sibling_path)
                for sibling_path in itertools.islice(
                    path.parent.expanduser().iterdir(), 100
                )
                if sibling_path.name.lower().startswith(name.lower())
                and is_directory_or_fitsfile(sibling_path)
            ]
            if possible_paths:
                possible_paths.sort(key=str.__len__)
                suggestion = possible_paths[0]
                if "~" in value:
                    home = str(Path("~").expanduser())
                    suggestion = suggestion.replace(home, "~")
                return suggestion

        except FileNotFoundError:
            pass
        return None

================
File: misfits/utils.py
================
from asyncio import sleep
from contextlib import asynccontextmanager
from contextlib import contextmanager
from time import perf_counter
from typing import Callable

from textual.widget import Widget


@contextmanager
def catchtime() -> Callable[[], float]:
    """A context manager for measuring computing times."""
    t1 = t2 = perf_counter()
    yield lambda: t2 - t1
    t2 = perf_counter()


@asynccontextmanager
async def disable_inputs(loading: Widget, disabled: list[Widget], delay: float = 0.25):
    """
    Disables input and shows a loading animation while tables are read into memory.

    :param disabled:
    :param loading:
    :param delay: seconds delay between end of loading indicator and
    file input prompt release.
    :return:
    """
    for widget in disabled:
        widget.disabled = True
    loading.loading = True
    yield
    loading.loading = False
    # we wait a bit before releasing the input because quick, repeated sends can
    # cause a tab to not load properly
    await sleep(delay)
    for widget in disabled:
        widget.disabled = False

================
File: tests/ascii_i4-i20.fits
================
SIMPLE  =                    T / conforms to FITS standard                      BITPIX  =                    8 / array data type                                NAXIS   =                    0 / number of array dimensions                     EXTEND  =                    T                                                  END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             XTENSION= 'TABLE   '           / ASCII table extension                          BITPIX  =                    8 / array data type                                NAXIS   =                    2 / number of array dimensions                     NAXIS1  =                   50 / length of dimension 1                          NAXIS2  =                    5 / length of dimension 2                          PCOUNT  =                    0 / number of group parameters                     GCOUNT  =                    1 / number of groups                               TFIELDS =                    5 / number of table fields                         TTYPE1  = 'col0    '                                                            TFORM1  = 'I8      '                                                            TBCOL1  =                    1                                                  TTYPE2  = 'col1    '                                                            TFORM2  = 'I8      '                                                            TBCOL2  =                    9                                                  TTYPE3  = 'col2    '                                                            TFORM3  = 'I10     '                                                            TBCOL3  =                   17                                                  TTYPE4  = 'col3    '                                                            TFORM4  = 'I20     '                                                            TBCOL4  =                   27                                                  TTYPE5  = 'col4    '                                                            TFORM5  = 'I4      '                                                            TBCOL5  =                   47                                                  END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    8      16       256               65536 256 8388608167772162147483647 92233720368547758078192-4194304-8388608-536870912-9223372036854775808-512      10      20        30                  40  50 8388608167772162147483647 92233720368547758078192

================
File: tests/ascii.fits
================
SIMPLE  =                    T / file does conform to FITS standard             BITPIX  =                   16 / number of bits per data pixel                  NAXIS   =                    0 / number of data axes                            EXTEND  =                    T / FITS dataset may contain extensions            COMMENT   FITS (Flexible Image Transport System) format defined in Astronomy andCOMMENT   Astrophysics Supplement Series v44/p363, v44/p371, v73/p359, v73/p365.COMMENT   Contact the NASA Science Office of Standards and Technology for the   COMMENT   FITS Definition document #100 and other FITS information.             END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             XTENSION= 'TABLE   '           / ASCII table extension                          BITPIX  =                    8 / 8-bit ASCII characters                         NAXIS   =                    2 / 2-dimensional ASCII table                      NAXIS1  =                   16 / width of table in characters                   NAXIS2  =                    5                                                  PCOUNT  =                    0 / no group parameters (required keyword)         GCOUNT  =                    1 / one data group (required)                      TFIELDS =                    2                                                  TTYPE1  = 'a       '           / label for field   1                            TBCOL1  =                    1 / beginning column of field   1                  TFORM1  = 'E10.4   '           / Fortran-77 format of field                     TUNIT1  = 'pixels  '           / physical unit of field                         TTYPE2  = 'b       '           / label for field   2                            TBCOL2  =                   12 / beginning column of field   2                  TFORM2  = 'I5      '           / Fortran-77 format of field                     TUNIT2  = 'counts  '           / physical unit of field                         TNULL1  = '*       '           / string representing an undefined value         TNULL2  = '*       '           / string representing an undefined value         HISTORY   This FITS file was created by the FCREATE task.                       HISTORY   fcreate3.0d at 23/4/97 9:21:56.                                       END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             .10123E+02    37.52000E+01    23.15610E+02    17*          *    .34500E+03   345

================
File: tests/history_header.fits
================
SIMPLE  =                    T / conforms to FITS standard                      BITPIX  =                    8 / array data type                                NAXIS   =                    0 / number of array dimensions                     HISTORY I updated this file on 02/03/2011                                       HISTORY I updated this file on 02/04/2011                                       END

================
File: tests/LICENSE.rst
================
Copyright (c) 2011-2024, Astropy Developers

All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.
* Neither the name of the Astropy Team nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

================
File: tests/o4sp040b0_raw.fits
================
SIMPLE  =                    T / Fits standard                                  BITPIX  =                   16 / Bits per pixel                                 NAXIS   =                    0 / Number of axes                                 EXTEND  =                    T / File may contain extensions                    ORIGIN  = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator        IRAF-TLM= '14:58:02 (23/02/2007)' / Time of last modification                   NEXTEND =                    6 / Number of standard extensions                  DATE    = '2007-02-23T19:57:58' / date this file was written (yyyy-mm-dd)       FILENAME= 'o4sp040b0_raw.fits                     ' / name of file              FILETYPE= 'SCI      '          / type of data found in data file                                                                                                TELESCOP= 'HST'                / telescope used to acquire data                 INSTRUME= 'STIS  '             / identifier for instrument used to acquire data EQUINOX =               2000.0 / equinox of celestial coord. system                                                                                                           / DATA DESCRIPTION KEYWORDS                                                                                                                       ROOTNAME= 'o4sp040b0                         ' / rootname of the observation setPRIMESI = 'STIS  '             / instrument designated as prime                                                                                                               / TARGET INFORMATION                                                                                                                              TARGNAME= 'HD101998                      ' / proposer's target name             RA_TARG =   1.761216666667E+02 / right ascension of the target (deg) (J2000)    DEC_TARG=   4.851611111111E+01 / declination of the target (deg) (J2000)                                                                                                      / PROPOSAL INFORMATION                                                                                                                            PROPOSID=                 7932 / PEP proposal identifier                        LINENUM = '3.120          '    / proposal logsheet line number                  PR_INV_L= 'Leitherer                     ' / last name of principal investigatorPR_INV_F= 'Claus               ' / first name of principal investigator         PR_INV_M= '                    ' / middle name / initial of principal investigat                                                                                              / SUMMARY EXPOSURE INFORMATION                                                                                                                    TDATEOBS= '1998-04-20'         / UT date of start of first exposure in file     TTIMEOBS= '18:38:15'           / UT start time of first exposure in file        TEXPSTRT=   5.092377657113E+04 / start time (MJD) of 1st exposure in file       TEXPEND =       50923.77948761 / end time (MJD) of last exposure in the file    TEXPTIME=                 120. / total exposure time (seconds)                                                                                                                / TARGET OFFSETS (POSTARGS)                                                                                                                       POSTARG1=             0.000000 / POSTARG in axis 1 direction                    POSTARG2=             0.000000 / POSTARG in axis 2 direction                                                                                                                  / DIAGNOSTIC KEYWORDS                                                                                                                             OVERFLOW=                    0 / Number of science data overflows               CAL_VER = '                        ' / CALSTIS code version                     PROCTIME=   5.279415092593E+04 / Pipeline processing time (MJD)                                                                                                               / SCIENCE INSTRUMENT CONFIGURATION                                                                                                                CFSTATUS= 'SUPPORTED  '        / configuration status (support., avail., eng.)  OBSTYPE = 'SPECTROSCOPIC '     / observation type - imaging or spectroscopic    OBSMODE = 'ACCUM     '         / operating mode                                 PHOTMODE= '                                                  ' / observation conSCLAMP  = 'NONE     '          / lamp status, NONE or name of lamp which is on  LAMPSET = '0.0   '             / spectral cal lamp current value (milliamps)    NRPTEXP =                    1 / number of repeat exposures in set: default 1   SUBARRAY=                    F / data from a subarray (T) or full frame (F)     DETECTOR= 'CCD       '         / detector in use: NUV-MAMA, FUV-MAMA, or CCD    OPT_ELEM= 'G750M   '           / optical element in use                         APERTURE= '0.2X0.2         '   / aperture name                                  PROPAPER= '0.2X0.2         '   / proposed aperture name                         FILTER  = 'Clear             ' / filter in use                                  APER_FOV= '0.2x0.2         '   / aperture field of view                         CENWAVE =                 8561 / central wavelength of spectrum                 CRSPLIT =                    4 / number of cosmic ray split exposures                                                                                                         / ENGINEERING PARAMETERS                                                                                                                          CCDAMP  = 'D  '                / CCD amplifier read out (A,B,C,D)               CCDGAIN =                    4 / commanded gain of CCD                          CCDOFFST=                    3 / commanded CCD bias offset                                                                                                                    / READOUT DEFINITION PARAMETERS                                                                                                                   CENTERA1=                  532 / subarray axis1 center pt in unbinned dect. pix CENTERA2=                  523 / subarray axis2 center pt in unbinned dect. pix SIZAXIS1=                 1062 / subarray axis1 size in unbinned detector pixelsSIZAXIS2=                 1044 / subarray axis2 size in unbinned detector pixelsBINAXIS1=                    1 / axis1 data bin size in unbinned detector pixelsBINAXIS2=                    1 / axis2 data bin size in unbinned detector pixels                                                                                              / CALIBRATION SWITCHES: PERFORM, OMIT, COMPLETE                                                                                                   DQICORR = 'PERFORM '           / data quality initialization                    ATODCORR= 'OMIT    '           / correct for A to D conversion errors           BLEVCORR= 'PERFORM '           / subtract bias level computed from overscan img BIASCORR= 'PERFORM '           / Subtract bias image                            CRCORR  = 'PERFORM '           / combine observations to reject cosmic rays     RPTCORR = 'OMIT    '           / add individual repeat observations             EXPSCORR= 'PERFORM '           / process individual observations after cr-rejectDARKCORR= 'PERFORM '           / Subtract dark image                            FLATCORR= 'PERFORM '           / flat field data                                SHADCORR= 'OMIT    '           / apply shutter shading correction               STATFLAG=                    T / Calculate statistics?                          WAVECORR= 'PERFORM '           / use wavecal to adjust wavelength zeropoint     X1DCORR = 'PERFORM '           / Perform 1-D spectral extraction                BACKCORR= 'PERFORM '           / subtract background (sky and interorder)       HELCORR = 'PERFORM '           / convert to heliocenttric wavelengths           DISPCORR= 'PERFORM '           / apply 2-dimensional dispersion solutions       FLUXCORR= 'PERFORM '           / convert to absolute flux units                 X2DCORR = 'PERFORM '           / rectify 2-D spectral image                                                                                                                   / CALIBRATION REFERENCE FILES                                                                                                                     BPIXTAB = 'otab$h1v11475o_bpx.fits' / bad pixel table                           DARKFILE= 'oref$jce11265o_drk.fits' / dark image file name                      PFLTFILE= 'oref$k2910265o_pfl.fits' / pixel to pixel flat field file name       DFLTFILE= 'N/A     '                / delta flat field file name                LFLTFILE= '        '                / low order flat                            PHOTTAB = 'otab$k9f1452qo_pht.fits' / Photometric throughput table              APERTAB = 'otab$laf13369o_apt.fits' / relative aperture throughput table        CCDTAB  = 'otab$k2g1502eo_ccd.fits' / CCD calibration parameters                ATODTAB = 'N/A     '                / analog to digital correction file         BIASFILE= 'oref$k5h1101io_bia.fits' / bias image file name                      SHADFILE= 'N/A     '                / shutter shading correction file           CRREJTAB= 'otab$j3m1403io_crr.fits' / cosmic ray rejection parameters           WAVECAL = 'o4sp040b0_wav.fits     ' / wavecal image file name                   APDESTAB= 'otab$m9a16591o_apd.fits' / aperture description table                SPTRCTAB= 'otab$l2j0137so_1dt.fits' / spectrum trace table                      DISPTAB = 'otab$l2j0137to_dsp.fits' / dispersion coefficient table              INANGTAB= 'otab$h5s11397o_iac.fits' / incidence angle correction table          LAMPTAB = 'otab$l421050oo_lmp.fits' / template calibration lamp spectra table   SDCTAB  = 'otab$laf1336ao_sdc.fits' / 2-D spatial distortion correction table   XTRACTAB= 'otab$lb21642oo_1dx.fits' / parameters for 1-D spectral extraction tabPCTAB   = 'otab$n2o1817ko_pct.fits' / Photometry correction table               WBIAFILE= 'oref$k5h1101io_bia.fits' / associated wavecal bias image file name   WCPTAB  = 'otab$lag1815lo_wcp.fits' / wavecal parameters table                  TDSTAB  = 'N/A     '                / time-dependent sensitivity algorithm used                                                                                               / COSMIC RAY REJECTION ALGORITHM PARAMETERS                                                                                                       MEANEXP =             0.000000 / reference exposure time for parameters         SCALENSE=             0.000000 / multiplicative scale factor applied to noise   INITGUES= '   '                / initial guess method (MIN or MED)              SKYSUB  = '    '               / sky value subtracted (MODE or NONE)            CRSIGMAS= '               '    / statistical rejection criteria                 CRRADIUS=             0.000000 / rejection propagation radius (pixels)          CRTHRESH=             0.000000 / rejection propagation threshold                BADINPDQ=                    0 / data quality flag bits to reject               REJ_RATE=                  0.0 / rate at which pixels are affected by cosmic rayCRMASK  =                    F / flag CR-rejected pixels in input files (T/F)                                                                                                 / CALIBRATED ENGINEERING PARAMETERS                                                                                                               ATODGAIN=             0.000000 / calibrated CCD amplifier gain value            READNSE =             0.000000 / calibrated CCD read noise value                                                                                                              / TARGET ACQUISITION DATASET IDENTIFIERS                                                                                                          ACQNAME = 'o4sp04DHT '         / rootname of acquisition exposure               ACQTYPE = '               '    / type of acquisition                            PEAKNAM1= 'o4sp04EMT '         / rootname of 1st peakup exposure                PEAKNAM2= 'o4sp04ENT '         / rootname of 2nd peakup exposure                                                                                                              / PATTERN KEYWORDS                                                                                                                                PATTERN1= 'NONE                    ' / primary pattern type                     P1_SHAPE= '                  ' / primary pattern shape                          P1_PURPS= '          '         / primary pattern purpose                        P1_NPTS =                    0 / number of points in primary pattern            P1_PSPAC=             0.000000 / point spacing for primary pattern (arc-sec)    P1_LSPAC=             0.000000 / line spacing for primary pattern (arc-sec)     P1_ANGLE=             0.000000 / angle between sides of parallelogram patt (deg)P1_FRAME= '         '          / coordinate frame of primary pattern            P1_ORINT=             0.000000 / orientation of pattern to coordinate frame (degP1_CENTR= '   '                / center pattern relative to pointing (yes/no)                                                                                                 / ARCHIVE SEARCH KEYWORDS                                                                                                                         BANDWID =                572.0 / bandwidth of the data                          SPECRES =               7780.0 / approx. resolving power at central wavelength  CENTRWV =               8561.0 / central wavelength of the data                 MINWAVE =               8275.0 / minimum wavelength in spectrum                 MAXWAVE =               8847.0 / maximum wavelength in spectrum                 PLATESC =                 0.05 / plate scale (arcsec/pixel)                                                                                                                   / PAPER PRODUCT SUPPORT KEYWORDS                                                                                                                  PROPTTL1= 'Spectral Purity and slit throughputs for the First Order Spectroscop'PROPTTL2= 'ic Modes                                                            'OBSET_ID= '04'                 / observation set id                             TARDESCR= 'STAR                                                                'MTFLAG  = ' '                  / moving target flag; T if it is a moving target PARALLAX=   0.000000000000E+00 / target parallax from proposal                  MU_RA   =   0.000000000000E+00 / target proper motion from proposal (degrees RA)MU_DEC  =   0.000000000000E+00 / target proper motion from proposal (deg. DEC)  MU_EPOCH= 'J2000.0'            / epoch of proper motion from proposal                                                                                                         / ASSOCIATION KEYWORDS                                                                                                                            ASN_ID  = 'O4SP040B0 '         / unique identifier assigned to association      ASN_TAB = 'o4sp040b0_asn.fits     ' / name of the association table             LRC_XSTS=                    F                                                  LRC_FAIL=                    F                                                  HISTORY   Copied from o4sp040b0_raw.fits                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        END                                                                             XTENSION= 'IMAGE   '           / Image extension                                BITPIX  =                   16 / Bits per pixel                                 NAXIS   =                    2 / Number of axes                                 NAXIS1  =                   62 / Axis length                                    NAXIS2  =                   44 / Axis length                                    PCOUNT  =                    0 / No 'random' parameters                         GCOUNT  =                    1 / Only one group                                 ORIGIN  = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator        EXTNAME = 'SCI     '           / Extension name                                 EXTVER  =                    1 / Extension version                              INHERIT =                    F / Inherits global header                         DATE    = '2007-02-23T19:57:58' / Date FITS file was generated                  IRAF-TLM= '14:57:58 (23/02/2007)' / Time of last modification                   ROOTNAME= 'o4sp040b0                         ' / rootname of the observation setEXPNAME = 'o4sp04ezq                ' / exposure identifier                     BUNIT   = 'COUNTS            ' / brightness units                               ASN_MTYP= 'CRSPLIT     '       / Role of the Member in the Association                                                                                                        / World Coordinate System and Related Parameters                                                                                                  WCSAXES =                    2 / number of World Coordinate System axes         CRPIX1  =              535.384 / x-coordinate of reference pixel                CRPIX2  =               536.67 / y-coordinate of reference pixel                CRVAL1  =   8.561000000000E+03 / first axis value at reference pixel            CRVAL2  =   0.000000000000E+00 / second axis value at reference pixel           CTYPE1  = 'LAMBDA  '           / the coordinate type for the first axis         CTYPE2  = 'ANGLE   '           / the coordinate type for the second axis        CD1_1   =                0.554 / partial of first axis coordinate w.r.t. x      CD1_2   =                  0.0 / partial of first axis coordinate w.r.t. y      CD2_1   =                  0.0 / partial of second axis coordinate w.r.t. x     CD2_2   =          1.38889E-05 / partial of second axis coordinate w.r.t. y     LTV1    =                 19.0 / offset in X to subsection start                LTV2    =                 20.0 / offset in Y to subsection start                LTM1_1  =                  1.0 / reciprocal of sampling rate in X               LTM2_2  =                  1.0 / reciprocal of sampling rate in Y               RA_APER =   1.761216666667E+02 / RA of aperture reference position              DEC_APER=   4.851611111111E+01 / Declination of aperture reference position     PA_APER =   1.143617019653E+02 / Position Angle of reference aperture center (deDISPAXIS=                    1 / dispersion axis; 1 = axis 1, 2 = axis 2, none  CUNIT1  = 'angstrom'           / units of first coordinate value                CUNIT2  = 'deg     '           / units of second coordinate value                                                                                                             / OFFSETS FROM ASSOCIATED WAVECAL                                                                                                                 SHIFTA1 =             0.000000 / Spectrum shift in AXIS1 calculated from WAVECALSHIFTA2 =             0.000000 / Spectrum shift in AXIS2 calculated from WAVECAL                                                                                              / EXPOSURE INFORMATION                                                                                                                            ORIENTAT=              114.362 / position angle of image y axis (deg. e of n)   SUNANGLE=           113.360931 / angle between sun and V1 axis                  MOONANGL=           131.851257 / angle between moon and V1 axis                 SUN_ALT =           -52.544285 / altitude of the sun above Earth's limb         FGSLOCK = 'FINE              ' / commanded FGS lock (FINE,COARSE,GYROS,UNKNOWN)                                                                                 DATE-OBS= '1998-04-20'         / UT date of start of observation (yyyy-mm-dd)   TIME-OBS= '18:38:15'           / UT time of start of observation (hh:mm:ss)     EXPSTART=   5.092377657113E+04 / exposure start time (Modified Julian Date)     EXPEND  =   5.092377691835E+04 / exposure end time (Modified Julian Date)       EXPTIME =            30.000000 / exposure duration (seconds)--calculated        EXPFLAG = 'NORMAL       '      / Exposure interruption indicator                                                                                                              / PATTERN KEYWORDS                                                                                                                                PATTSTEP=                    0 / position number of this point in the pattern                                                                                                 / REPEATED EXPOSURES INFO                                                                                                                         NCOMBINE=                    1 / number of image sets combined during CR rejecti                                                                                              / DATA PACKET INFORMATION                                                                                                                         FILLCNT =                    0 / number of segments containing fill             ERRCNT  =                    0 / number of segments containing errors           PODPSFF =                    F / podps fill present (T/F)                       STDCFFF =                    F / ST DDF fill present (T/F)                      STDCFFP = '0x5569'             / ST DDF fill pattern (hex)                                                                                                                    / ENGINEERING PARAMETERS                                                                                                                          OSWABSP =              1234272 / Slit Wheel Absolute position                   OMSCYL1P=                 4008 / Mode select cylinder 1 position                OMSCYL3P=                 1177 / Mode select cylinder 3 position                OMSCYL4P=                 5297 / Mode select cylinder 4 position                OCBABAV =              26.7024 / (V) CEB A&B Amp Bias                           OCBCDAV =              26.6827 / (V) CEB C&D amp bias                           OCBLGCDV=             -3.38871 / (V) CEB last gate C&D                          OCBSWALV=             -6.00587 / (V) CB summing well A Lo                       OCBRCDLV=            0.0487692 / (V) CB reset gate CD Lo                        OCCDHTAV=            -1.000000 / average CCD housing temperature (degC)                                                                                                       / IMAGE STATISTICS AND DATA QUALITY FLAGS                                                                                                         NGOODPIX=              1108728 / number of good pixels                          SDQFLAGS=                31743 / serious data quality flags                     GOODMIN =               1486.0 / minimum value of good pixels                   GOODMAX =              14113.0 / maximum value of good pixels                   GOODMEAN=          1526.857056 / mean value of good pixels                      SNRMIN  =             0.000000 / minimum signal to noise of good pixels         SNRMAX  =             0.000000 / maximum signal to noise of good pixels         SNRMEAN =             0.000000 / mean value of signal to noise of good pixels   SOFTERRS=                    0 / number of soft error pixels (DQF=1)            MEANDARK=                  0.0 / average of the dark values subtracted          MEANBLEV=                  0.0 / average of all bias levels subtracted                                                                                                        / PHOTOMETRY KEYWORDS                                                                                                                             SPORDER =                    1 / Spectral order                                 DIFF2PT =                  1.0 / Diffuse to point source conversion factor      CONT2EML=             0.000000 / Intensity conversion: continuum -> emission    SCALE_A1=             0.000000 / Size of one pixel (arcsec) along dispersion axiOMEGAPIX=             0.000000 / Solid angle (arcsec**2) subtended by one pixel BZERO   =                32768                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  END                                                                                                                                                                                                                                             …ã…å…á…à…ä…â…å…ä…à…à…å…å…â…â…à…ã…à…á…ã…æ…ä…â…æ…ä…ç…æ…æ…æ…æ…ã…å…ä…á…ã…ä…å…æ…å…ã…å…ä…å…æ…æ…å…æ…ä…ä…ä…æ…ã…æ…è…å…é…ç…ä…å…ã…æ…å…ã…ä…ã…å…ç…ã…å…ä…ç…ä…æ…è…ä…á…â…å…è…ä…ç…ç…æ…ä…è…å…ã…å…ä…ä…ä…ç…ã…æ…ç…æ…ã…å…å…ç…ç…å…ã…ä…â…ã…æ…æ…å…ã…æ…å…ã…æ…å…å…ä…å…ã…å…ä…ã…ã…æ…ä…ç…ä…æ…å…ã…ä…ã…å…ã…å…ä…â…æ…æ…ç…å…å…è…ç…å…å…ã…à…ä…ã…æ…ä…æ…æ…ä…ä…æ…æ…ç…æ…ã…ä…ä…ä…å…â…ä…æ…å…ç…ç…è…ä…ã…ä…ã…â…ä…æ…å…æ…ã…å…ã…ã…å…æ…ã…å…å…å…å…å…å…å…å…ç…å…ç…ä…ç…ã…ã…ç…ã…å…å…ä…å…å…æ…ã…å…æ…æ…ä…å…ã…æ…è…å…ã…è…ç…å…å…ç…é…å…æ…å…ä…á…ä…ã…ä…ä…ä…ã…â…ä…ä…æ…æ…ä…ä…å…å…ä…ç…ä…ä…ä…å…ä…æ…æ…ã…ä…à…ä…æ…æ…å…â…â…ä…ç…ç…ç…æ…æ…ã…ä…æ…å…å…ä…ã…è…æ…å…å…ç…ç…æ…ã…å…â…ã…ä…â…å…ç…ç…ä…æ…ä…è…è…ã…á…ä…â…ã…è…æ…ä…è…ä…ä…ä…å…å…æ…å…è…è…ã…ä…æ…æ…å…ã…ä…æ…æ…å…ç…ã…ã…å…ã…å…è…ä…å…æ…å…å…è…ä…ç…æ…ä…è…ä…ä…ä…ã…ä…ä…ã…ä…æ…è…ç…è…å…ä…á…ä…ä…å…å…á…æ…æ…ã…â…å…ç…â…ã…æ…ã…ä…á…ä…ç…å…æ…æ…ã…ç…è…å…ã…ä…æ…ã…ã…á…æ…æ…å…ã…è…ç…æ…ã…â…á…ä…á…ä…å…æ…ã…ä…ç…ä…ä…ã…æ…ä…ã…å…å…â…ç…ä…ä…ä…ç…æ…ç…æ…ä…å…ã…å…ä…á…ä…å…ä…ä…ç…ä…æ…ä…æ…ç…ã…ä…å…ã…å…â…ã…æ…è…è…ä…æ…ä…ç…ã…æ…æ…â…ç…ã…å…ç…á…æ…å…ç…ã…ç…ä…ã…ä…ã…ã…ã…ã…â…å…æ…æ…å…æ…à…è…å…ã…ã…ä…å…ã…ä…ã…æ…ä…ã…è…æ…ä…ä…á…ä…ä…á…æ…ä…æ…å…å…ä…ä…ã…è…å…å…ã…æ…ä…ã…ã…è…ã…æ…è…æ…ã…ã…ã…ä…å…ã…ä…â…ã…â…ç…ä…ç…á…ã…ã…á…ç…ä…æ…å…ã…ã…å…â…æ…å…ä…ä…ä…å…ä…ã…ç…â…â…æ…å…æ…ä…å…ä…æ…å…ç…å…á…ä…ä…ä…ã…â…ã…ä…å…æ…ã…æ…ç…ç…æ…â…ã…ã…ä…æ…å…Þ…æ…â…æ…æ…ç…ç…å…å…æ…å…ã…æ…å…ä…á…æ…æ…å…å…ä…ä…ä…á…ã…å…à…ä…å…ç…ä…ç…è…ã…æ…å…ä…æ…á…å…ã…å…å…ä…ã…æ…æ…ç…ä…ä…å…ä…ã…å…å…ã…å…ã…ë…å…á…æ…å…æ…ç…ä…å…â…ä…ä…æ…ä…ã…ã…á…è…æ…ç…ä…â…ä…æ…á…æ…á…ã…ä…å…ã…ä…å…ã…ç…á…â…å…æ…ä…ä…ã…æ…ä…ã…å…ç…ä…æ…è…à…ã…ã…ç…è…å…æ…å…ã…ä…æ…é…ä…å…å…å…ä…â…æ…æ…ä…ã…ã…è…å…å…ç…ç…ä…ã…ä…ã…æ…æ…å…ç…ä…ä…ç…è…ç…â…ä…ä…à…ä…ä…ä…ä…â…ç…ä…ä…å…ä…ä…ã…è…ã…å…â…ä…ä…â…ã…ä…æ…å…ç…æ…â…â…â…ä…ã…ã…ã…ã…ã…æ…æ…ä…ç…ç…æ…ã…ã…ã…â…ä…è…ä…ä…æ…ã…æ…ä…ç…ä…æ…ä…æ…ã…å…å…â…ã…â…ä…è…è…å…ã…å…å…â…ä…æ…ç…ä…ä…â…ç…æ…ç…ç…ä…â…å…å…á…ã…ã…ä…ä…ã…ã…æ…æ…æ…ç…å…å…á…ã…æ…æ…å…ä…å…å…ä…ä…å…ä…å…ã…ã…ä…á…æ…ã…å…å…ã…ç…å…ä…æ…ç…ä…ç…ç…å…ä…å…ä…æ…æ…å…æ…ç…ä…ä…â…ä…å…ã…å…ã…ä…ä…å…æ…æ…æ…ã…â…â…å…å…ä…ä…æ…â…ã…ã…ä…ç…å…æ…è…ä…á…é…æ…å…â…å…ä…ã…æ…ä…æ…ä…æ…æ…â…ã…ã…å…æ…ä…â…â…è…å…å…å…ç…æ…ä…ä…á…ã…ã…á…à…å…â…ä…ã…æ…ä…æ…ã…å…ã…è…ä…å…ç…ä…ã…ç…ä…è…æ…ç…ä…æ…ã…æ…å…á…æ…â…ä…á…æ…ä…å…ç…ä…ä…ä…ä…å…â…ã…â…ä…á…è…æ…ä…ä…å…å…ç…å…ä…â…ã…ã…ä…ä…ã…è…è…å…ä…ä…ç…ä…ä…æ…ã…â…ä…å…æ…æ…è…å…è…æ…ä…å…å…ã…â…ã…å…ä…á…ã…å…â…ä…è…â…à…ã…á…á…à…à…ã…à…â…á…ß…á…á…Þ…á…Ý…Ú…Ü…Ï…ä…ã…å…å…â…â…å…â…ä…á…ã…à…à…ã…å…á…â…á…ã…å…ã…è…ã…ã…à…â…ã…á…æ…æ…ä…ä…è…æ…å…ä…æ…å…ã…ä…ã…å…å…ä…æ…ä…ä…ç…å…å…æ…ä…ã…ã…æ…å…å…å…å…ã…â…ã…ã…æ…ã…å…ä…â…å…æ…æ…ä…ä…ã…ç…æ…å…ä…ã…æ…ã…ä…ä…å…ä…è…æ…ã…ã…æ…ä…ä…æ…æ…â…æ…ä…ã…æ…ã…å…ä…ã…æ…ã…ä…ã…á…ã…ã…ä…ä…ä…ç…ä…å…ã…ã…ä…ç…ä…ã…â…ã…ã…ä…æ…ä…ä…ä…å…æ…ä…å…æ…ä…ã…å…á…ã…ã…ä…å…å…ä…è…ä…è…æ…ç…ã…á…æ…å…ä…ç…ä…á…æ…è…ä…ä…ã…ã…å…á…ä…á…ä…á…ã…æ…ä…å…ä…ä…ã…ä…ä…ä…å…ã…æ…ã…è…ä…å…ç…ç…ã…è…à…á…æ…â…ä…â…â…ã…æ…æ…ç…é…å…ç…ä…å…ä…æ…ã…ä…å…å…è…ã…ç…æ…ä…ç…å…ã…ä…ä…ã…ä…â…ä…ã…å…ç…ç…å…ã…æ…æ…ã…ç…å…ä…ä…ã…ã…â…ä…ä…ã…æ…æ…å…ä…ç…ä…ã…ä…å…ä…ã…ç…ä…ç…ç…è…æ…ä…ä…å…ç…ä…ä…ç…å…å…æ…ç…å…æ…ä…ä…ã…ã…ç…æ…ä…ä…ä…å…å…ã…ä…å…ä…ç…å…â…ä…è…ä…æ…å…ã…å…æ…å…ã…ä…æ…ã…ã…â…ã…è…æ…ã…á…æ…é…è…å…ä…æ…å…å…ä…å…ã…ä…æ…å…å…å…ç…æ…æ…æ…ã…ã…æ…ä…â…â…ä…æ…é…ã…å…ä…æ…è…ã…è…á…ä…ã…ã…â…â…æ…æ…æ…ä…ç…ã…ä…å…ä…å…ä…æ…å…â…â…â…ä…ä…ç…å…å…ã…ã…â…ã…ã…æ…á…å…ä…é…æ…ä…æ…ä…â…æ…å…à…å…ã…ä…ã…å…á…æ…ç…ç…æ…ç…ä…ä…æ…ã…ç…á…ã…ä…ä…ä…æ…æ…ç…á…ã…å…â…â…ã…â…æ…ã…ç…æ…ã…ç…ä…å…å…æ…ã…æ…ã…æ…â…ã…å…ä…é…æ…ä…ç…å…è…ä…ä…å…æ…ä…ã…ä…ä…ä…è…ä…â…å…ä…ä…â…ä…ã…å…æ…å…ä…å…è…æ…å…å…à…è…ä…ã…ä…á…ä…â…ç…ç…å…æ…ä…æ…ç…ä…ä…ã…ã…ä…ç…å…ä…å…ç…ç…å…ç…å…ä…æ…ã…ã…ä…ã…ç…è…ä…ç…è…ç…å…ä…ä…æ…ä…à…ä…â…ä…ä…ä…å…å…ç…ã…ä…å…å…ã…ä…ä…å…ä…å…å…æ…å…é…æ…å…â…æ…æ…ä…á…ä…æ…â…ã…æ…é…ç…ä…æ…ã…æ…ã…ã…â…å…å…â…å…æ…æ…å…å…ç…æ…ä…ç…å…å…æ…ä…ã…æ…à…å…å…å…æ…å…ä…å…å…ä…ã…å…å…á…ç…è…å…æ…ä…â…ä…å…á…ã…â…ä…ã…ã…æ…ä…é…è…æ…æ…æ…ã…ç…å…ã…å…ä…ã…æ…ã…æ…å…æ…å…è…å…å…å…å…Ý…æ…å…å…æ…ä…ã…ä…å…æ…è…ä…ä…æ…ã…å…â…ä…æ…â…ä…ä…ã…æ…ä…æ…ç…ä…ã…ã…ä…è…æ…ä…ä…ç…ä…æ…å…å…ç…ç…â…â…à…â…æ…è…å…æ…ä…ä…â…å…ã…ã…ä…â…ç…ä…â…ä…æ…æ…è…å…ä…è…ã…å…ä…ä…å…ä…â…ã…ã…å…æ…æ…æ…å…ç…æ…ã…æ…á…â…ä…ä…å…ä…æ…ä…ã…ç…ä…æ…ä…â…á…â…ã…å…å…à…ä…æ…ä…å…ä…ã…ä…á…ä…ã…ä…æ…å…ç…å…ä…æ…ä…å…å…å…ä…â…ã…æ…å…æ…ä…ä…ä…æ…æ…æ…ã…â…ä…á…æ…ä…ä…é…æ…å…ä…å…è…ç…å…ã…æ…å…ä…æ…å…ç…ä…ã…ç…å…ä…ä…å…ä…æ…æ…ã…â…ç…æ…ã…á…ç…æ…ä…å…ã…å…à…ã…ç…å…å…ä…ä…å…ä…ã…æ…ã…æ…ç…ä…æ…å…å…æ…ä…å…æ…ç…å…å…å…å…ã…ã…ä…â…à…ã…ä…â…æ…å…ã…æ…æ…ã…è…ç…ä…å…ã…å…ä…ã…ã…ä…å…æ…ã…ã…á…æ…ä…á…æ…æ…ä…å…æ…å…å…å…ç…æ…è…à…ç…å…ä…å…â…å…á…å…è…é…è…æ…ç…æ…ä…å…ã…à…â…ä…å…å…å…ç…ä…é…æ…ã…å…ç…ä…ç…å…á…ä…ä…ç…ã…å…ä…æ…æ…æ…ã…à…â…æ…ä…å…ä…ã…å…ã…å…å…æ…æ…è…æ…â…ã…â…â…ä…æ…ç…è…æ…å…ä…ä…å…å…æ…ä…å…â…â…å…ä…æ…å…ç…ä…é…é…ä…æ…ã…ã…â…â…è…è…å…ã…ä…ä…á…å…ã…å…å…à…â…å…á…ä…å…æ…æ…ä…æ…å…ä…ä…ä…æ…å…ä…ã…ã…é…ç…æ…å…å…å…ã…æ…å…ã…ä…ä…ä…ä…å…è…å…ç…å…æ…å…å…å…ä…á…á…ã…ã…ã…á…ã…à…ã…â…à…à…ã…ß…à…ß…á…à…á…Ý…ß…Ú…á…Ò…á…ã…á…â…á…ä…ä…ä…â…å…ä…â…â…â…å…ä…à…à…â…æ…å…ä…ä…ä…ã…æ…ä…á…ä…ã…ç…å…ä…ã…ä…å…ä…å…á…à…å…ã…é…ä…å…æ…å…å…æ…æ…å…å…ã…ã…æ…â…æ…ä…ä…ã…à…æ…æ…ç…æ…ç…æ…ã…æ…æ…á…ã…ä…â…ã…æ…æ…å…æ…ä…ê…æ…æ…ç…à…å…ã…ä…ä…ä…å…è…ä…ä…æ…æ…ä…å…ä…â…â…å…â…ä…å…ã…ã…å…å…ç…æ…å…ä…å…å…â…ã…å…ä…ç…æ…æ…æ…å…ç…ä…æ…ç…ã…ã…å…ç…ã…ã…â…â…è…ä…ç…â…ä…ã…æ…å…ã…å…ä…ç…ç…è…æ…ã…ä…æ…ç…ç…å…ã…ã…ã…ä…æ…ä…ä…å…å…è…è…æ…å…â…ç…â…ä…ä…ä…ç…è…å…ç…æ…ã…æ…å…â…ä…ä…ã…è…ã…æ…â…ã…ã…å…æ…å…æ…å…ã…å…ã…â…ç…æ…ä…æ…ä…ç…á…æ…ç…å…ç…ä…å…ã…â…ã…ä…ä…å…ç…ã…å…å…å…å…å…ä…â…ã…ç…ä…ç…ç…æ…ç…ã…è…æ…â…ä…è…ã…ã…å…ç…æ…å…â…ç…æ…å…æ…å…ä…å…å…æ…ä…å…è…æ…æ…ã…æ…å…á…â…â…ã…á…â…æ…æ…ã…ä…â…å…ç…ã…æ…ä…å…à…ä…ã…ä…ä…ä…å…æ…å…å…ä…æ…å…ä…æ…â…â…å…æ…ä…ç…å…æ…å…æ…ã…ä…æ…æ…å…ã…ç…å…æ…ä…å…æ…ç…ä…é…ã…ã…ä…â…ä…ä…æ…å…ä…ä…å…æ…æ…å…â…ê…å…ã…á…â…å…å…å…ã…â…ä…å…ç…é…ä…å…ã…ã…á…ã…å…ä…â…æ…æ…ç…ç…â…ä…ç…å…á…æ…á…æ…ã…ã…ä…ã…æ…é…ã…æ…ä…æ…ä…æ…ã…á…ã…â…ä…æ…ä…è…æ…ä…ä…â…æ…å…ã…ã…å…ã…æ…ã…å…å…ä…ä…ã…â…æ…â…ã…å…â…ã…ä…æ…å…è…ç…å…å…ä…ã…ä…æ…å…å…å…å…ä…å…æ…å…ç…ä…ä…å…å…ä…æ…ã…â…æ…æ…æ…ä…å…â…ç…ä…å…ã…ã…å…á…ã…ä…æ…æ…è…ç…å…ä…ä…å…ã…ä…å…ä…å…ã…å…ä…æ…æ…é…æ…æ…á…ã…ê…æ…ã…ä…ç…æ…å…ä…ç…å…ä…ã…ã…ä…â…ä…ä…á…ä…ä…æ…å…æ…ä…ä…ç…å…â…ã…å…ä…ã…ç…å…å…è…æ…ç…æ…ã…æ…å…å…ä…ã…å…ä…æ…ä…è…ä…ä…ä…æ…å…å…â…å…ä…á…è…å…ã…æ…æ…ä…ä…ã…æ…å…ä…ä…ã…â…å…å…æ…æ…å…å…æ…ã…ç…á…ã…ä…ä…å…â…ä…ä…è…ä…ç…ç…ä…ã…æ…ã…ä…â…æ…å…ã…æ…å…ä…ç…æ…â…æ…ç…å…å…ã…ä…æ…ã…ä                                                                                                                                                                                                                                                                                                                XTENSION= 'IMAGE   '           / Image extension                                BITPIX  =                   16 / Bits per pixel                                 NAXIS   =                    0 / Number of axes                                 PCOUNT  =                    0 / No 'random' parameters                         GCOUNT  =                    1 / Only one group                                 ORIGIN  = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator        EXTNAME = 'ERR     '           / Extension name                                 EXTVER  =                    1 / Extension version                              INHERIT =                    F / Inherits global header                         DATE    = '2007-02-23T19:57:58' / Date FITS file was generated                  IRAF-TLM= '14:57:58 (23/02/2007)' / Time of last modification                   ROOTNAME= 'o4sp040b0                         ' / rootname of the observation setEXPNAME = 'o4sp04ezq                ' / exposure identifier                     BUNIT   = 'COUNTS            ' / brightness units                               NPIX1   =                   62 / length of constant array axis 1                NPIX2   =                   44 / length of constant array axis 2                PIXVALUE=                  0.0 / values of pixels in constant array                                                                                                           / World Coordinate System and Related Parameters                                                                                                  WCSAXES =                    2 / number of World Coordinate System axes         CRPIX1  =              535.384 / x-coordinate of reference pixel                CRPIX2  =               536.67 / y-coordinate of reference pixel                CRVAL1  =   8.561000000000E+03 / first axis value at reference pixel            CRVAL2  =   0.000000000000E+00 / second axis value at reference pixel           CTYPE1  = 'LAMBDA  '           / the coordinate type for the first axis         CTYPE2  = 'ANGLE   '           / the coordinate type for the second axis        CD1_1   =                0.554 / partial of first axis coordinate w.r.t. x      CD1_2   =                   0. / partial of first axis coordinate w.r.t. y      CD2_1   =                   0. / partial of second axis coordinate w.r.t. x     CD2_2   =  1.38889000000000E-5 / partial of second axis coordinate w.r.t. y     LTV1    =                  19. / offset in X to subsection start                LTV2    =                  20. / offset in Y to subsection start                LTM1_1  =                   1. / reciprocal of sampling rate in X               LTM2_2  =                   1. / reciprocal of sampling rate in Y               RA_APER =   1.761216666667E+02 / RA of aperture reference position              DEC_APER=   4.851611111111E+01 / Declination of aperture reference position     PA_APER =   1.143617019653E+02 / Position Angle of reference aperture center (deDISPAXIS=                    1 / dispersion axis; 1 = axis 1, 2 = axis 2, none  CUNIT1  = 'angstrom'           / units of first coordinate value                CUNIT2  = 'deg     '           / units of second coordinate value                                                                                                             / OFFSETS FROM ASSOCIATED WAVECAL                                                                                                                 SHIFTA1 =             0.000000 / Spectrum shift in AXIS1 calculated from WAVECALSHIFTA2 =             0.000000 / Spectrum shift in AXIS2 calculated from WAVECAL                                                                                              / NOISE MODEL KEYWORDS                                                                                                                            NOISEMOD= '                                        ' / noise model equation     NOISCOF1=   0.000000000000E+00 / noise coefficient 1                            NOISCOF2=   0.000000000000E+00 / noise coefficient 2                            NOISCOF3=   0.000000000000E+00 / noise coefficient 3                            NOISCOF4=   0.000000000000E+00 / noise coefficient 4                            NOISCOF5=   0.000000000000E+00 / noise coefficient 5                                                                                                                          / IMAGE STATISTICS AND DATA QUALITY FLAGS                                                                                                         NGOODPIX=              1108728 / number of good pixels                          SDQFLAGS=                31743 / serious data quality flags                     GOODMIN =               1486.0 / minimum value of good pixels                   GOODMAX =              14113.0 / maximum value of good pixels                   GOODMEAN=          1526.857056 / mean value of good pixels                      LTM2_1  =                   0.                                                  LTM1_2  =                   0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  END                                                                             XTENSION= 'IMAGE   '           / Image extension                                BITPIX  =                   16 / Bits per pixel                                 NAXIS   =                    0 / Number of axes                                 PCOUNT  =                    0 / No 'random' parameters                         GCOUNT  =                    1 / Only one group                                 ORIGIN  = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator        EXTNAME = 'DQ      '           / Extension name                                 EXTVER  =                    1 / Extension version                              INHERIT =                    F / Inherits global header                         DATE    = '2007-02-23T19:57:58' / Date FITS file was generated                  IRAF-TLM= '14:57:58 (23/02/2007)' / Time of last modification                   ROOTNAME= 'o4sp040b0                         ' / rootname of the observation setEXPNAME = 'o4sp04ezq                ' / exposure identifier                     BUNIT   = 'UNITLESS          ' / brightness units                               NPIX1   =                   62 / length of constant array axis 1                NPIX2   =                   44 / length of constant array axis 2                PIXVALUE=                    0 / values of pixels in constant array                                                                                                           / World Coordinate System and Related Parameters                                                                                                  WCSAXES =                    2 / number of World Coordinate System axes         CRPIX1  =              535.384 / x-coordinate of reference pixel                CRPIX2  =               536.67 / y-coordinate of reference pixel                CRVAL1  =   8.561000000000E+03 / first axis value at reference pixel            CRVAL2  =   0.000000000000E+00 / second axis value at reference pixel           CTYPE1  = 'LAMBDA  '           / the coordinate type for the first axis         CTYPE2  = 'ANGLE   '           / the coordinate type for the second axis        CD1_1   =                0.554 / partial of first axis coordinate w.r.t. x      CD1_2   =                   0. / partial of first axis coordinate w.r.t. y      CD2_1   =                   0. / partial of second axis coordinate w.r.t. x     CD2_2   =  1.38889000000000E-5 / partial of second axis coordinate w.r.t. y     LTV1    =                  19. / offset in X to subsection start                LTV2    =                  20. / offset in Y to subsection start                LTM1_1  =                   1. / reciprocal of sampling rate in X               LTM2_2  =                   1. / reciprocal of sampling rate in Y               RA_APER =   1.761216666667E+02 / RA of aperture reference position              DEC_APER=   4.851611111111E+01 / Declination of aperture reference position     PA_APER =   1.143617019653E+02 / Position Angle of reference aperture center (deDISPAXIS=                    1 / dispersion axis; 1 = axis 1, 2 = axis 2, none  CUNIT1  = 'angstrom'           / units of first coordinate value                CUNIT2  = 'deg     '           / units of second coordinate value                                                                                                             / OFFSETS FROM ASSOCIATED WAVECAL                                                                                                                 SHIFTA1 =             0.000000 / Spectrum shift in AXIS1 calculated from WAVECALSHIFTA2 =             0.000000 / Spectrum shift in AXIS2 calculated from WAVECALLTM2_1  =                   0.                                                  LTM1_2  =                   0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  END                                                                             XTENSION= 'IMAGE   '           / Image extension                                BITPIX  =                   16 / Bits per pixel                                 NAXIS   =                    2 / Number of axes                                 NAXIS1  =                   62 / Axis length                                    NAXIS2  =                   44 / Axis length                                    PCOUNT  =                    0 / No 'random' parameters                         GCOUNT  =                    1 / Only one group                                 ORIGIN  = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator        EXTNAME = 'SCI     '           / Extension name                                 EXTVER  =                    2 / Extension version                              INHERIT =                    F / Inherits global header                         DATE    = '2007-02-23T19:57:59' / Date FITS file was generated                  IRAF-TLM= '14:57:58 (23/02/2007)' / Time of last modification                   ROOTNAME= 'o4sp040b0                         ' / rootname of the observation setEXPNAME = 'o4sp04f0q                ' / exposure identifier                     BUNIT   = 'COUNTS            ' / brightness units                               ASN_MTYP= 'CRSPLIT     '       / Role of the Member in the Association                                                                                                        / World Coordinate System and Related Parameters                                                                                                  WCSAXES =                    2 / number of World Coordinate System axes         CRPIX1  =              535.384 / x-coordinate of reference pixel                CRPIX2  =               536.67 / y-coordinate of reference pixel                CRVAL1  =   8.561000000000E+03 / first axis value at reference pixel            CRVAL2  =   0.000000000000E+00 / second axis value at reference pixel           CTYPE1  = 'LAMBDA  '           / the coordinate type for the first axis         CTYPE2  = 'ANGLE   '           / the coordinate type for the second axis        CD1_1   =                0.554 / partial of first axis coordinate w.r.t. x      CD1_2   =                  0.0 / partial of first axis coordinate w.r.t. y      CD2_1   =                  0.0 / partial of second axis coordinate w.r.t. x     CD2_2   =          1.38889E-05 / partial of second axis coordinate w.r.t. y     LTV1    =                 19.0 / offset in X to subsection start                LTV2    =                 20.0 / offset in Y to subsection start                LTM1_1  =                  1.0 / reciprocal of sampling rate in X               LTM2_2  =                  1.0 / reciprocal of sampling rate in Y               RA_APER =   1.761216666667E+02 / RA of aperture reference position              DEC_APER=   4.851611111111E+01 / Declination of aperture reference position     PA_APER =   1.143617019653E+02 / Position Angle of reference aperture center (deDISPAXIS=                    1 / dispersion axis; 1 = axis 1, 2 = axis 2, none  CUNIT1  = 'angstrom'           / units of first coordinate value                CUNIT2  = 'deg     '           / units of second coordinate value                                                                                                             / OFFSETS FROM ASSOCIATED WAVECAL                                                                                                                 SHIFTA1 =             0.000000 / Spectrum shift in AXIS1 calculated from WAVECALSHIFTA2 =             0.000000 / Spectrum shift in AXIS2 calculated from WAVECAL                                                                                              / EXPOSURE INFORMATION                                                                                                                            ORIENTAT=              114.362 / position angle of image y axis (deg. e of n)   SUNANGLE=           113.360229 / angle between sun and V1 axis                  MOONANGL=           131.857269 / angle between moon and V1 axis                 SUN_ALT =           -58.053928 / altitude of the sun above Earth's limb         FGSLOCK = 'FINE              ' / commanded FGS lock (FINE,COARSE,GYROS,UNKNOWN)                                                                                 DATE-OBS= '1998-04-20'         / UT date of start of observation (yyyy-mm-dd)   TIME-OBS= '18:39:29'           / UT time of start of observation (hh:mm:ss)     EXPSTART=   5.092377742742E+04 / exposure start time (Modified Julian Date)     EXPEND  =   5.092377777464E+04 / exposure end time (Modified Julian Date)       EXPTIME =            30.000000 / exposure duration (seconds)--calculated        EXPFLAG = 'NORMAL       '      / Exposure interruption indicator                                                                                                              / PATTERN KEYWORDS                                                                                                                                PATTSTEP=                    0 / position number of this point in the pattern                                                                                                 / REPEATED EXPOSURES INFO                                                                                                                         NCOMBINE=                    1 / number of image sets combined during CR rejecti                                                                                              / DATA PACKET INFORMATION                                                                                                                         FILLCNT =                    0 / number of segments containing fill             ERRCNT  =                    0 / number of segments containing errors           PODPSFF =                    F / podps fill present (T/F)                       STDCFFF =                    F / ST DDF fill present (T/F)                      STDCFFP = '0x5569'             / ST DDF fill pattern (hex)                                                                                                                    / ENGINEERING PARAMETERS                                                                                                                          OSWABSP =              1234272 / Slit Wheel Absolute position                   OMSCYL1P=                 4008 / Mode select cylinder 1 position                OMSCYL3P=                 1177 / Mode select cylinder 3 position                OMSCYL4P=                 5297 / Mode select cylinder 4 position                OCBABAV =              26.7024 / (V) CEB A&B Amp Bias                           OCBCDAV =              26.7024 / (V) CEB C&D amp bias                           OCBLGCDV=             -3.37895 / (V) CEB last gate C&D                          OCBSWALV=             -5.99614 / (V) CB summing well A Lo                       OCBRCDLV=            0.0390036 / (V) CB reset gate CD Lo                        OCCDHTAV=            -1.000000 / average CCD housing temperature (degC)                                                                                                       / IMAGE STATISTICS AND DATA QUALITY FLAGS                                                                                                         NGOODPIX=              1108728 / number of good pixels                          SDQFLAGS=                31743 / serious data quality flags                     GOODMIN =               1484.0 / minimum value of good pixels                   GOODMAX =              14089.0 / maximum value of good pixels                   GOODMEAN=          1526.973145 / mean value of good pixels                      SNRMIN  =             0.000000 / minimum signal to noise of good pixels         SNRMAX  =             0.000000 / maximum signal to noise of good pixels         SNRMEAN =             0.000000 / mean value of signal to noise of good pixels   SOFTERRS=                    0 / number of soft error pixels (DQF=1)            MEANDARK=                  0.0 / average of the dark values subtracted          MEANBLEV=                  0.0 / average of all bias levels subtracted                                                                                                        / PHOTOMETRY KEYWORDS                                                                                                                             SPORDER =                    1 / Spectral order                                 DIFF2PT =                  1.0 / Diffuse to point source conversion factor      CONT2EML=             0.000000 / Intensity conversion: continuum -> emission    SCALE_A1=             0.000000 / Size of one pixel (arcsec) along dispersion axiOMEGAPIX=             0.000000 / Solid angle (arcsec**2) subtended by one pixel BZERO   =                32768                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  END                                                                                                                                                                                                                                             …á…à…ã…á…â…å…ã…ã…ã…ã…ã…ä…â…â…â…à…à…á…ã…ç…ç…å…æ…ã…ä…å…æ…å…ã…ã…ã…ç…ã…â…æ…ä…ã…ä…æ…ç…ã…æ…æ…å…å…â…ä…æ…è…ä…ä…å…æ…è…å…å…â…æ…á…å…ã…ä…â…ä…æ…æ…ê…è…ä…ä…å…ç…ã…à…å…ã…ã…é…å…æ…ç…ç…ä…æ…æ…ã…â…æ…ã…å…å…æ…æ…ä…ç…ê…æ…å…ã…ä…ç…à…â…ä…ä…å…ä…å…ä…ç…ã…æ…ç…æ…ä…å…ã…æ…ä…ç…è…æ…ä…æ…æ…ã…å…æ…æ…ç…å…ã…å…â…ã…æ…ç…ç…å…æ…æ…ã…å…ä…â…æ…é…ç…å…ç…å…ç…å…æ…æ…ã…ã…ã…æ…ä…ã…å…ä…å…ç…ã…æ…æ…å…ä…â…ç…ä…ä…æ…ã…ã…æ…æ…æ…æ…ç…å…â…æ…å…ã…ã…ä…á…æ…ä…æ…ä…ã…æ…å…å…å…ß…å…ã…á…ä…ã…ä…å…ã…é…ã…æ…å…ã…â…ã…å…æ…å…â…ä…ã…æ…è…ç…è…å…ã…æ…ä…è…æ…á…á…ã…ä…ç…å…è…è…å…æ…ä…æ…ç…æ…ã…ä…å…è…ä…ç…è…é…å…ç…ä…æ…å…ç…ä…ä…å…á…ã…é…ç…ç…ä…ç…ä…ä…ç…æ…ä…ä…ä…ä…å…ã…ã…æ…æ…æ…æ…å…ç…ä…ã…ä…ã…ä…ä…ä…ä…è…ã…ä…ä…ã…ä…æ…è…ç…å…è…â…ä…å…æ…å…æ…å…ä…ç…ê…ä…ç…ã…ä…ä…à…ã…æ…ä…æ…æ…æ…å…è…æ…á…æ…ä…ä…ã…ä…è…á…æ…ä…ä…ä…æ…å…æ…ä…æ…â…æ…â…å…æ…è…á…å…ä…æ…å…æ…ã…ã…ä…ä…æ…ä…æ…å…æ…ç…ä…æ…å…å…ä…ã…ä…ä…ä…å…æ…æ…æ…æ…æ…ç…æ…è…æ…ß…ç…ã…ä…å…ç…ä…å…æ…å…æ…è…å…å…â…â…ã…ä…ä…å…ã…ã…ä…æ…ä…å…è…å…â…ä…ä…å…ä…ä…å…ä…å…ç…æ…ã…è…ä…ã…â…å…ä…å…ä…ä…ä…ä…ä…å…ç…ç…å…è…ã…ä…â…é…å…ã…ç…å…æ…ã…å…ä…å…å…å…æ…ä…å…â…ä…æ…ä…æ…â…å…æ…æ…å…æ…å…â…å…æ…ä…ä…ã…â…è…â…æ…ç…ã…ä…æ…å…å…ä…ã…ã…ç…à…æ…è…è…å…ä…æ…ä…å…á…ã…ç…ä…â…æ…å…ç…ç…ç…å…ä…ç…å…æ…ä…æ…â…ä…á…æ…ç…ã…æ…ä…å…å…ä…å…ä…æ…æ…ã…ã…å…æ…ä…å…ä…è…å…ã…ç…ä…æ…ä…ç…â…ã…â…ä…ä…æ…è…å…å…å…æ…á…ã…æ…â…ã…æ…ä…ã…å…å…ä…æ…ç…ç…ä…ä…å…ä…å…æ…ä…à…â…ç…ã…ç…å…å…ä…ä…â…å…ä…â…å…è…å…æ…è…å…ä…å…æ…ä…á…æ…ä…á…å…ä…æ…á…å…æ…å…æ…å…ä…ã…â…ä…â…á…æ…è…å…æ…å…ä…æ…ã…ä…ã…å…ä…ã…ä…á…å…ä…ä…ç…ä…ç…ç…ä…ã…ã…æ…æ…ä…à…ã…ã…è…ä…å…æ…ç…ä…å…ä…ã…ä…å…å…ä…æ…ä…â…å…å…æ…ç…å…ä…ç…å…ç…ä…ã…å…å…å…è…ã…è…æ…è…æ…ã…æ…á…ä…ä…à…ä…å…å…å…æ…å…ç…æ…ä…ã…ä…ä…æ…ä…å…ä…ä…ä…â…è…æ…æ…á…å…à…ã…â…ä…æ…æ…á…å…ç…ã…â…á…ã…ä…å…ä…å…æ…ã…ç…æ…å…æ…ä…ä…ä…ä…ã…ã…ä…ä…æ…å…â…å…ä…æ…æ…å…ç…æ…ã…è†p…ø…æ…ã…ã…ä…ä…æ…æ…ä…ç…ã…æ…ä…ä…ä…ç…ä…æ…ç…å…ä…ä…ç…å…æ…ç…ç…æ…ã…ã…å…å…ã…è…ã…æ…ã…å…â…å…ã…ç…ã…ã…å…ä…å…ä…ã…ä…æ…ä…æ…é…ã…ã…ä…æ…ç…ä…á…â…ç…ä…ä…ã…å…ä…ä…è…æ…ä…ä…å…ã…ã…ã…å…ä…è…æ…ä…ã…é…è…á…ä…ã…ä…á…ä…é…á…ã…å…æ…ä…æ…æ…å…æ…á…ä…ä…ä…å…å…ã…æ…ã…æ…ã…å…ã…ç…á…ã…å…æ…æ…ã…ã…ä…æ…ã…æ…æ…æ…æ…ä…ä…ã…â…ä…ä…â…ã…ç…å…æ…ç…é…ã…ä…ã…ä…ä…æ…ä…å…ã…ã…å…ã…ç…ã…å…è…á…ä…ä…ã…ä…ã…å…ä…ç…ã…ã…ã…ä…ä…ä…â…ä…â…ç…æ…ä…â…å…å…æ…ç…ç…é…è…æ…ç…æ…ç…à…â…â…æ…â…ä…ç…á…æ…ç…ä…ã…ã…ã…ä…ã…æ…æ…ä…å…ä…æ…ä…ã…ä…â…ã…æ…ã…å…æ…ã…å…ä…ç…ä…å…ä…å…â…ä…å…ã…ç…å…à…æ…å…å…å…ã…å…å…ç…æ…å…æ…ã…å…å…å…æ…æ…ä…ç…ç…ç…å…å…æ…ã…æ…ç…ä…æ…â…ä…ä…å…å…â…å…æ…â…æ…á…â…Þ…Þ…à…à…á…â…à…â…â…á…à…ß…à…Þ…Ü…×…à…Õ…ä…ã…ä…á…á…â…å…á…á…à…â…ã…ß…â…à…á…ä…ã…ã…æ…å…æ…å…å…ç…è…ä…å…æ…ç…æ…æ…è…ã…â…ä…å…ß…ß…ä…â…æ…ä…æ…ä…ç…è…å…å…æ…â…ç…ã…ä…ä…å…è…å…å…ã…æ…è…ä…å…å…ç…ã…å…â…æ…ã…ä…æ…å…å…å…å…å…å…ä…å…ã…æ…å…ä…á…ç…ç…æ…ä…æ…å…å…ä…ã…á…ä…ä…ç…ä…ã…ä…ä…ç…ç…å…ß…å…ä…æ…ä…ã…ä…ä…è…æ…ç…ã…æ…â…å…ã…ç…â…ã…á…ä…ä…ç…è…ç…æ…é…å…æ…ã…å…á…å…ã…ã…ä…å…æ…æ…æ…å…å…å…ä…æ…ä…ã…ä…æ…å…â…å…ç…ç…æ…ã…æ…ç…ç…ã…ã…å…à…ä…è…æ…á…ã…ä…ç…æ…ç…ä…ä…ã…å…ä…ä…æ…å…ä…å…ã…ä…æ…ä…æ…å…ä…æ…å…â…æ…ä…æ…ä…æ…è…ã…ç…è…å…ã…à…ä…å…ã…â…â…ä…ä…ä…å…â…æ…ã…æ…â…â…ã…à…å…æ…á…è…æ…ä…ç…ç…å…ã…ä…ã…æ…á…å…ç…ä…ã…ç…ä…æ…ä…ç…ç…ã…ä…æ…å…ã…å…å…ã…ä…ä…ç…æ…æ…å…ä…ã…â…ã…å…æ…á…æ…æ…å…â…ç…å…ã…â…ä…å…á…ã…æ…æ…ã…ã…ä…å…é…ç…è…ä…ã…â…å…ç…æ…â…ä…ã…ã…ä…ç…ã…æ…ä…ä…ã…å…ä…ã…ä…ä…ä…è…å…å…ã…æ…å…æ…â…ã…ç…æ…å…ã…á…â…â…ç…è…æ…æ…ä…å…à…å…ç…â…ä…å…å…â…ã…è…æ…ä…æ…è…å…ä…æ…ã…â…å…æ…ä…æ…ä…å…æ…æ…ç…æ…è…á…ã…ã…ã…æ…â…è…æ…é…æ…å…å…æ…å…æ…å…ä…ä…ã…ä…ã…å…å…æ…å…é…å…æ…ã…ä…å…ä…ç…å…ä…ç…æ…á…â…æ…æ…â…â…æ…å…å…æ…â…ã…æ…æ…ç…å…å…ä…å…ç…â…æ…ä…á…ã…å…æ…æ…æ…ä…ä…å…æ…æ…ç…ç…ç…ä…æ…ä…ã…ã…ä…å…ã…å…ç…å…ã…å…ä…å…å…ä…ä…ã…ç…ä…ä…å…â…ä…á…â…æ…ã…ã…ä…ä…ã…å…å…â…å…è…å…æ…å…ç…á…ä…æ…ã…è…ä…ä…ä…æ…æ…â…â…å…ä…æ…å…ä…ä…æ…ä…ä…ã…æ…ä…æ…ã…ä…è…å…á…ä…æ…ç…â…ä…æ…è…ç…æ…ä…å…â…á…ç…ã…å…â…ã…å…æ…ã…é…á…ä…æ…æ…ä…â…ã…á…ã…ã…æ…æ…å…ç…æ…ç…æ…å…â…æ…å…â…ã…à…ã…ä…ç…å…ã…ã…ä…å…å…ä…é…ã…ã…å…å…â…æ…ç…å…å…æ…æ…æ…æ…å…æ…ä…á…ã…ä…æ…ä…å…å…ç…ä…ä…æ…ä…ã…ä…æ…ã…ä…ä…â…ä…å…ä…ä…æ…å…è…ã…ä…â…à…â…ä…è…æ…á…ã…ç…æ…æ…ã…â…æ…å…â…â…â…ä…å…á…ã…ä…ã…æ…å…å…ã…æ…æ…â…à…ã…æ…æ…á…æ…æ…â…æ…é…å…å…ä…ä…æ…ä…â…å…ä…å…æ…å…è…æ…ä…å…â…å…ä…æ…ã…å…ç…ã…è…ã…ä…è…ã…ä…ç…ä…å…ä…å…å…å…ä…ç…æ…å…ã…å…å…â…æ…â…ä…ã…â…ä…ä…ç…å…å…ç…ã…å…á…ã…ã…æ…â…â…æ…æ…å…è…ç…é…â…ã…ã…â…å…å…ä…ä…ã…ä…è…æ…á…â…â…å…â…ç…â…ä…æ…æ…á…ã…ã…é…å…â…æ…æ…æ…â…à…å…ã…â…ä…è…é‡&†…è…è…ç…å…å…â…æ…ä…ä…æ…ã…æ…ã…è…ä…ç…ä…å…ã…ã…ä…â…â…ã…ç…å…ä…ã…å…è…ç…ç…ç…â…ã…å…æ…å…ä…ã…æ…æ…æ…ç…ç…ä…â…æ…ä…á…æ…å…æ…ã…ã…å…å…æ…æ…æ…ä…ä…ã…ç…ä…ç…ä…â…ã…å…é…ç…å…ä…ã…ä…å…è…ã…á…ä…æ…å…á…ä…ä…æ…è…å…ä…å…å…æ…ã…ä…ç…è…ã…è…ç…æ…æ…å…å…å…æ…æ…ã…â…á…ä…ä…ä…å…ä…ã…å…é…ä…å…ã…å…â…ä…â…ä…ä…ä…å…æ…ã…ä…å…ã…å…è…ã…ä…å…æ…æ…â…ä…ã…ã…ã…å…ä…æ…á…è…ä…ã…å…å…å…å…ç…ä…â…å…ç…ç…ç…æ…á…ã…â…æ…å…â…ã…å…ã…ä…å…ç…ä…å…ä…ã…â…â…â…å…ä…â…ä…ã…æ…æ…à…ä…ç…â…å…ã…ã…â…ä…å…ç…æ…æ…ç…å…ä…ã…â…â…ã…æ…ä…æ…ç…ä…ä…ä…å…æ…â…ä…ä…â…å…æ…ã…æ…ã…æ…ç…ä…æ…á…å…å…å…å…ã…æ…å…æ…è…æ…æ…è…ç…ç…æ…ä…æ…ä…æ…è…â…å…å…ç…ä…ä…æ…ä…æ…ä…å…ä…å…å…å…â…á…ä…æ…ä…ã…ã…á…ä…á…à…à…à…à…ß…á…á…ß…â…ã…ã…à…Ý…Ø…à…Ñ…à…â…â…ã…å…å…ä…ä…ã…ã…á…ã…â…à…ß…Þ…å…ã…å…ã…ä…å…ã…ä…å…å…å…á…ã…è…å…ç…å…å…è…å…ä…å…å…ã…è…æ…á…å…ä…á…è…è…å…å…ã…ä…á…ä…ç…ä…ã…â…ç…å…æ…ä…ä…æ…å…ä…æ…ã…ç…æ…ä…æ…æ…ä…æ…è…ä…ä…æ…ç…ä…ê…å…ã…ã…ã…æ…ã…æ…ã…ã…å…ç…ç…æ…æ…ã…ä…ã…â…á…â…ã…å…ä…ã…â…æ…æ…ä…ç…å…ã…ã…æ…ä…æ…æ…ä…ç…ç…æ…å…å…æ…â…ä…á…ä…ã…æ…ä…è…æ…æ…æ…æ…ä…å…å…å…ä…ä…å…å…ç…ã…å…å…ã…è…æ…ç…ä…å…ä…å…æ…æ…ä…æ…ã…å…å…æ…æ…è…å…è…ä…á…å…å…å…å…ä…ã…ã…ã…ç…å…å…å…â…â…å…ã…è…ä…æ…ä…â…æ…ã…ç…å…ä…å…å…æ…ã…å…ã…è…å…ä…æ…ä…ã…ä…æ…ç…ä…ã…ã…ã…â…é…ç…ã…æ…æ…è…æ…æ…ã…æ…ä…á…ã…å…ã…å…å…ä…æ…ä…æ…æ…á…ã…å…ä…â…å…å…å…ä…å…å…å…æ…æ…ã…å…æ…ã…ä…ã…æ…å…å…æ…å…æ…æ…ç…ä…ã…â…ã…å…ä…ä…æ…ä…å…ä…æ…å…ã…å…ã…à…å…ã…ã…ä…ã…â…æ…ç…ä…ç…å…è…å…è…ä…ã…å…ä…å…æ…ã…ä…æ…ã…æ…å…ç…å…ã…ã…ä…ã…ç…ã…à…ã…æ…è…ã…ä…æ…ã…å…ä…â…ã…ä…ä…ã…ä…ä…é…ä…æ…ç…ä…ã…á…æ…ã…ã…å…å…ã…æ…æ…è…å…ã…ç…ã…â…ä…â…â…á…æ…ã…à…æ…ã…ç…å…æ…ä…ä…å…å…å…å…æ…ä…â…æ…ã…ä…æ…ä…æ…ä…à…å…æ…ã…å…å…ã…å…ä…ç…æ…ç…æ…æ…â…ã…å…å…à…â…ç…ä…æ…â…å…ç…ä…è…ä…ã…â…è…ä…ä…æ…å…æ…æ…é…è…æ…å…æ…æ…ç…â…ç…à…æ…ç…ä…ç…å…ç…ä…ç…æ…å…ç…ã…ã…â…å…æ…ã…ä…ä…ç…ã…è…æ…ç…ã…ã…å…å…á…å…ä…ä…ç…ã…ç…ä…ã…è…à…ç…ä…å…ç…ä…ä…ã…å…å…å…á…ä…ä…å…å…ã…ã…â…ã…æ…ä…æ…å…ä…ä…å…å…æ…æ…å…å…ã…ã…á…ã…æ…ã…å…è…æ…ç…é…å…ä…å…å…æ…à…ä…æ…ã…é…ä…å…æ…å…ç…á…ä…ä…æ…å…â…ã…ä…å…å…æ…ç…ä…å…â…æ…æ…å…å…å…å…æ…ã…å…æ…ã…ä…ç…è…ã…á…ä…ä…ä…â…æ…ä…ã…æ…å…æ…ä…â…ç…ä…ä…æ…á…ã…ç…ä…æ…ä…é…ä…å…ã…ã…ã…ã…ã…ä…ã…æ…å…ä…ä…ã…ã…ã…ä…å…æ…ä…å…ä                                                                                                                                                                                                                                                                                                                XTENSION= 'IMAGE   '           / Image extension                                BITPIX  =                   16 / Bits per pixel                                 NAXIS   =                    0 / Number of axes                                 PCOUNT  =                    0 / No 'random' parameters                         GCOUNT  =                    1 / Only one group                                 ORIGIN  = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator        EXTNAME = 'ERR     '           / Extension name                                 EXTVER  =                    2 / Extension version                              INHERIT =                    F / Inherits global header                         DATE    = '2007-02-23T19:57:59' / Date FITS file was generated                  IRAF-TLM= '14:57:59 (23/02/2007)' / Time of last modification                   ROOTNAME= 'o4sp040b0                         ' / rootname of the observation setEXPNAME = 'o4sp04f0q                ' / exposure identifier                     BUNIT   = 'COUNTS            ' / brightness units                               NPIX1   =                   62 / length of constant array axis 1                NPIX2   =                   44 / length of constant array axis 2                PIXVALUE=                  0.0 / values of pixels in constant array                                                                                                           / World Coordinate System and Related Parameters                                                                                                  WCSAXES =                    2 / number of World Coordinate System axes         CRPIX1  =              535.384 / x-coordinate of reference pixel                CRPIX2  =               536.67 / y-coordinate of reference pixel                CRVAL1  =   8.561000000000E+03 / first axis value at reference pixel            CRVAL2  =   0.000000000000E+00 / second axis value at reference pixel           CTYPE1  = 'LAMBDA  '           / the coordinate type for the first axis         CTYPE2  = 'ANGLE   '           / the coordinate type for the second axis        CD1_1   =                0.554 / partial of first axis coordinate w.r.t. x      CD1_2   =                   0. / partial of first axis coordinate w.r.t. y      CD2_1   =                   0. / partial of second axis coordinate w.r.t. x     CD2_2   =  1.38889000000000E-5 / partial of second axis coordinate w.r.t. y     LTV1    =                  19. / offset in X to subsection start                LTV2    =                  20. / offset in Y to subsection start                LTM1_1  =                   1. / reciprocal of sampling rate in X               LTM2_2  =                   1. / reciprocal of sampling rate in Y               RA_APER =   1.761216666667E+02 / RA of aperture reference position              DEC_APER=   4.851611111111E+01 / Declination of aperture reference position     PA_APER =   1.143617019653E+02 / Position Angle of reference aperture center (deDISPAXIS=                    1 / dispersion axis; 1 = axis 1, 2 = axis 2, none  CUNIT1  = 'angstrom'           / units of first coordinate value                CUNIT2  = 'deg     '           / units of second coordinate value                                                                                                             / OFFSETS FROM ASSOCIATED WAVECAL                                                                                                                 SHIFTA1 =             0.000000 / Spectrum shift in AXIS1 calculated from WAVECALSHIFTA2 =             0.000000 / Spectrum shift in AXIS2 calculated from WAVECAL                                                                                              / NOISE MODEL KEYWORDS                                                                                                                            NOISEMOD= '                                        ' / noise model equation     NOISCOF1=   0.000000000000E+00 / noise coefficient 1                            NOISCOF2=   0.000000000000E+00 / noise coefficient 2                            NOISCOF3=   0.000000000000E+00 / noise coefficient 3                            NOISCOF4=   0.000000000000E+00 / noise coefficient 4                            NOISCOF5=   0.000000000000E+00 / noise coefficient 5                                                                                                                          / IMAGE STATISTICS AND DATA QUALITY FLAGS                                                                                                         NGOODPIX=              1108728 / number of good pixels                          SDQFLAGS=                31743 / serious data quality flags                     GOODMIN =               1484.0 / minimum value of good pixels                   GOODMAX =              14089.0 / maximum value of good pixels                   GOODMEAN=          1526.973145 / mean value of good pixels                      LTM2_1  =                   0.                                                  LTM1_2  =                   0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  END                                                                             XTENSION= 'IMAGE   '           / Image extension                                BITPIX  =                   16 / Bits per pixel                                 NAXIS   =                    0 / Number of axes                                 PCOUNT  =                    0 / No 'random' parameters                         GCOUNT  =                    1 / Only one group                                 ORIGIN  = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator        EXTNAME = 'DQ      '           / Extension name                                 EXTVER  =                    2 / Extension version                              INHERIT =                    F / Inherits global header                         DATE    = '2007-02-23T19:57:59' / Date FITS file was generated                  IRAF-TLM= '14:57:59 (23/02/2007)' / Time of last modification                   ROOTNAME= 'o4sp040b0                         ' / rootname of the observation setEXPNAME = 'o4sp04f0q                ' / exposure identifier                     BUNIT   = 'UNITLESS          ' / brightness units                               NPIX1   =                   62 / length of constant array axis 1                NPIX2   =                   44 / length of constant array axis 2                PIXVALUE=                    0 / values of pixels in constant array                                                                                                           / World Coordinate System and Related Parameters                                                                                                  WCSAXES =                    2 / number of World Coordinate System axes         CRPIX1  =              535.384 / x-coordinate of reference pixel                CRPIX2  =               536.67 / y-coordinate of reference pixel                CRVAL1  =   8.561000000000E+03 / first axis value at reference pixel            CRVAL2  =   0.000000000000E+00 / second axis value at reference pixel           CTYPE1  = 'LAMBDA  '           / the coordinate type for the first axis         CTYPE2  = 'ANGLE   '           / the coordinate type for the second axis        CD1_1   =                0.554 / partial of first axis coordinate w.r.t. x      CD1_2   =                   0. / partial of first axis coordinate w.r.t. y      CD2_1   =                   0. / partial of second axis coordinate w.r.t. x     CD2_2   =  1.38889000000000E-5 / partial of second axis coordinate w.r.t. y     LTV1    =                  19. / offset in X to subsection start                LTV2    =                  20. / offset in Y to subsection start                LTM1_1  =                   1. / reciprocal of sampling rate in X               LTM2_2  =                   1. / reciprocal of sampling rate in Y               RA_APER =   1.761216666667E+02 / RA of aperture reference position              DEC_APER=   4.851611111111E+01 / Declination of aperture reference position     PA_APER =   1.143617019653E+02 / Position Angle of reference aperture center (deDISPAXIS=                    1 / dispersion axis; 1 = axis 1, 2 = axis 2, none  CUNIT1  = 'angstrom'           / units of first coordinate value                CUNIT2  = 'deg     '           / units of second coordinate value                                                                                                             / OFFSETS FROM ASSOCIATED WAVECAL                                                                                                                 SHIFTA1 =             0.000000 / Spectrum shift in AXIS1 calculated from WAVECALSHIFTA2 =             0.000000 / Spectrum shift in AXIS2 calculated from WAVECALLTM2_1  =                   0.                                                  LTM1_2  =                   0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  END

================
File: tests/verify.fits
================
SIMPLE  =                    T / conforms to FITS standard                      NAXIS   =                    0 / NUMBER OF AXES                                 BITPIX  =                    8 / BITS PER PIXEL                                 END

================
File: .gitattributes
================
* text=auto eol=lf

*.png binary
*.jpg binary
*.fits binary

================
File: .gitignore
================
**/.DS_Store
**/.idea
**/__pycache__
**/.ipynb_checkpoints
**/.mypy-cache
*venv*/
dist/

================
File: conda-env.yml
================
name: misfits
dependencies:
   - python>=3.11
   - pip
   - pip:
     - misfits

================
File: misfits-vhs.tape
================
Output assets/misfits.gif

Set FontSize 24
Set Width 1400
Set Height 900

Type "misfits tests/tests_high_energy_events.fits"
Enter

# show cheesy animation
Sleep 5s

# go to header tab
Tab
Sleep 1s

# select and open a node
Down
Sleep 1s
Enter
Sleep 1s

# expands all other nodes
Ctrl+S
Sleep 1s

# collapse nodes
Ctrl+S
Sleep 1s

# move to event tab
Tab
Sleep 1s

# scroll pages
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 50ms
Ctrl+N
Sleep 1s
Ctrl+A
Sleep 1s

# Use filter
Tab
Sleep 1s
Type "ENERGY > 666 & TIME > 81015781.70"
Sleep 500ms
Enter
Sleep 1s

# Swith to file tab
Tab
Sleep 1s

# Open file explorer
Ctrl+O
Sleep 1s
Down
Sleep 500ms
Down
Sleep 500ms
Down
Sleep 500ms
Down
Sleep 500ms
Up
Sleep 500ms
Up
Sleep 500ms
Up
Sleep 500ms
Up
Sleep 500ms
Escape
Sleep 1s

# Open Credits
Ctrl+J
Sleep 500ms
Sleep 5s

================
File: pyproject.toml
================
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "misfits"
version = "0.0.11"
authors = [
    { name="Giuseppe Dilillo", email="peppedilillo@gmail.com" },
]
description = "A FITS table viewer for the terminal."
keywords = ["FITS", "Flexible Image Transport System", "Astrophysics"]
readme = "README.md"
license = {file = "LICENSE.txt"}
requires-python = ">=3.11"
classifiers = [
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = [
    "astropy == 7.1.0",
    "pandas == 2.3.0",
    "textual == 3.5.0",
    "terminaltexteffects == 0.12.0",
    "click == 8.2.1",
]

[project.optional-dependencies]
dev = [
    "textual-dev>=1.5",
    "black>=24.8",
    "isort>=5.13",
    "mypy>=1.11",
    "ipython>=8.27",
]

[project.urls]
"Homepage" = "https://github.com/peppedilillo/misfits"

[project.scripts]
misfits = 'misfits.app:main'

================
File: README.md
================
![misfits's interface](https://github.com/peppedilillo/misfits/blob/main/assets/misfits.gif?raw=true)

# misfits

Misfits is a FITs table viewer for the terminal, written in python.
I want it to be snappy as hell and fully usable without touching the mouse.
It currently has some limitations (e.g. won't display VLA columns), but will work on them eventually.
It leverages astropy and pandas, and is built using [textual](https://www.textualize.io/).
Works on Linux, macOS and Windows. Performances on Windows are worse.
Renders best on modern terminals.

### Installation

#### Installing with `pip`

`pip install misfits`

Make sure to be installing into a fresh python>=3.11 environment!

#### Installing with `uv`

`uv tool install misfits`

With the other methods, you are supposed to activate the misfits environment first to use it.
Installing with uv you won't need that, and you will be able to call misfits from terminal with one line: `misfits`.
If you are unsure about uv: don't, give it a [try](https://docs.astral.sh/uv/getting-started/installation/)!
It is a great package manager from the people behind ruff and other python tools.

#### Installing with anaconda

`conda env create -f conda-env.yml`

Will create a new environment and install `misfits` in it.

### Usage

From the terminal, run `misfits path_to_file.fits`, `misfits .`, or simply `misfits`. 

### Contributing

Found a bug? Want a feature? Open an issue, a PR, or post in the discussion section of this repo.
