Skip to content

Converter & Schemas

The converter layer translates a validated StellarConfig into a sequence of KEPLER punch-card lines. Each card type is described by a CardSchema (containing an ordered list of CardField objects), and a dedicated converter class formats the relevant config data into that schema.

Pipeline

StellarConfig
PunchCardConverter.config_to_cards()
    ├─ MaterialCardConverter  → m  cards (one per ion)
    ├─ NetworkCardConverter   → net cards
    ├─ GridCardConverter      → g  cards (one per zone)
    └─ ParameterCardConverter → p  cards (one per parameter)
list[str]  →  write_punch_cards()  →  file

PunchCardConverter

PunchCardConverter

Converts a validated StellarConfig to the legacy punch-card deck.

Source code in src/keppy/converter.py
class PunchCardConverter:
    """Converts a validated StellarConfig to the legacy punch-card deck."""

    def config_to_cards(self, config: StellarConfig) -> list[str]:
        """Convert validated config to punch card lines"""
        cards: list[str] = []

        cards.extend(keyword_cards(config))

        for schema, build in CARD_PIPELINE:
            # config -> card field values
            card_data = build(config)

            # schema fields -> ASCII line(s)
            # logic to handle long list outputs or just single line
            for data in card_data if isinstance(card_data, list) else [card_data]:
                cards.append(schema.format_card(data))

        return cards

    def write_punch_cards(self, config: StellarConfig, output_path: Path) -> None:
        """Write punch card deck to file"""
        write_lines(self.config_to_cards(config), output_path)

config_to_cards

config_to_cards(config: StellarConfig) -> list[str]

Convert validated config to punch card lines

Source code in src/keppy/converter.py
def config_to_cards(self, config: StellarConfig) -> list[str]:
    """Convert validated config to punch card lines"""
    cards: list[str] = []

    cards.extend(keyword_cards(config))

    for schema, build in CARD_PIPELINE:
        # config -> card field values
        card_data = build(config)

        # schema fields -> ASCII line(s)
        # logic to handle long list outputs or just single line
        for data in card_data if isinstance(card_data, list) else [card_data]:
            cards.append(schema.format_card(data))

    return cards

write_punch_cards

write_punch_cards(config: StellarConfig, output_path: Path) -> None

Write punch card deck to file

Source code in src/keppy/converter.py
def write_punch_cards(self, config: StellarConfig, output_path: Path) -> None:
    """Write punch card deck to file"""
    write_lines(self.config_to_cards(config), output_path)

Schemas

Punch-card schemas are module-level constants in keppy.schemas. Each schema defines the card identifier and the ordered fields that appear on that line.

CardSchema

CardSchema dataclass

Defines the structure of a punch card. Card fields are specified IN ORDER.

Source code in src/keppy/schemas.py
@dataclass
class CardSchema:
    """Defines the structure of a punch card. Card fields are specified IN ORDER."""

    card_name: str  # internal card name
    card_id: str  # Identifier that appears in column 0-4
    fields: list[CardField]

    def format_card(self, data: str | dict) -> str:
        """Format a dictionary into a punch card line"""
        # pass through if its a string (already formatted keyword)
        if isinstance(data, str):
            return data
        else:
            # start with card ID
            line = self.card_id.ljust(5)
            line += " "

            # add fields
            for field in self.fields:
                if field.name in data:
                    line += field.format_value(data[field.name])
                    line += " "
                else:
                    line += " "
            return line
format_card
format_card(data: str | dict) -> str

Format a dictionary into a punch card line

Source code in src/keppy/schemas.py
def format_card(self, data: str | dict) -> str:
    """Format a dictionary into a punch card line"""
    # pass through if its a string (already formatted keyword)
    if isinstance(data, str):
        return data
    else:
        # start with card ID
        line = self.card_id.ljust(5)
        line += " "

        # add fields
        for field in self.fields:
            if field.name in data:
                line += field.format_value(data[field.name])
                line += " "
            else:
                line += " "
        return line

CardField

CardField dataclass

Defines a single field in a punch card

Source code in src/keppy/schemas.py
@dataclass
class CardField:
    """Defines a single field in a punch card"""

    name: str
    format_spec: str  # e.g., '10.5f', '5d', '10s'
    required: bool = True
    default: Optional[Any] = None
    description: str = "no description"

    def format_value(self, value: Any) -> str:
        """Format value according to punch card specifications"""
        format_str = f"{{{self.format_spec}}}"
        if value is None:
            return ""
        # Auto-detect format based on type
        if self.format_spec == "auto":
            if isinstance(value, bool):
                # Treat bool as int (True=1, False=0)
                format_str = "{:d}"
                value = int(value)
            elif isinstance(value, int):
                format_str = "{:d}"
            elif isinstance(value, float):
                # Use scientific notation for floats
                format_str = "{:16.9e}"
            else:
                # String or other
                format_str = "{:s}"
                value = str(value)
        else:
            # Use specified format
            format_str = f"{{:{self.format_spec}}}"

        return format_str.format(value)
format_value
format_value(value: Any) -> str

Format value according to punch card specifications

Source code in src/keppy/schemas.py
def format_value(self, value: Any) -> str:
    """Format value according to punch card specifications"""
    format_str = f"{{{self.format_spec}}}"
    if value is None:
        return ""
    # Auto-detect format based on type
    if self.format_spec == "auto":
        if isinstance(value, bool):
            # Treat bool as int (True=1, False=0)
            format_str = "{:d}"
            value = int(value)
        elif isinstance(value, int):
            format_str = "{:d}"
        elif isinstance(value, float):
            # Use scientific notation for floats
            format_str = "{:16.9e}"
        else:
            # String or other
            format_str = "{:s}"
            value = str(value)
    else:
        # Use specified format
        format_str = f"{{:{self.format_spec}}}"

    return format_str.format(value)

Built-in schemas

Name Card ID Purpose
NETWORK net Nuclear reaction network declaration
MATERIALS m Per-isotope mass fraction
GRID g Per-zone mass, temperature, density
PARAMETER p Runtime parameter override
COMMENT c Comment line

schemas

Punchcard (line in gen file) schemas. Describes effective conversion between TOML and card with formatting.

NETWORK module-attribute
NETWORK = CardSchema(card_name='NETWORK', card_id='net', fields=[CardField('num', '>2d'), CardField('elements', 's')])
MATERIALS module-attribute
MATERIALS = CardSchema(card_name='MATERIAL', card_id='m', fields=[CardField('name', '>5s'), CardField('abu', '24.16e'), CardField('ion', '>5s')])
GRID module-attribute
GRID = CardSchema(card_name='GRID', card_id='g', fields=[CardField('zone', '>4d'), CardField('ym', '17.10e'), CardField('num', '>4d'), CardField('comp', '>5s'), CardField('t', '16.9e'), CardField('rho', '16.9e'), CardField('aw', '16.9e', required=False), CardField('velocity', '16.9e', required=False)])
PARAMETER module-attribute
PARAMETER = CardSchema(card_name='PARAM', card_id='p', fields=[CardField('parameter', '<18s'), CardField('value', 'auto')])
COMMENT module-attribute
COMMENT = CardSchema(card_name='COMMENT', card_id='c', fields=[CardField('comment', '>4d', required=False)])