Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic conversion of a NASA GMAT script into its Nyx equivalent #141

Open
7 tasks
ChristopherRabotin opened this issue May 2, 2023 · 3 comments
Open
7 tasks
Labels
Interface: Python Kind: New feature This is a proposed new feature Priority: normal Status: Design Issue at Design phase of the quality assurance process Topic: Mission Design

Comments

@ChristopherRabotin
Copy link
Member

Co-author: Claude.

High level description

We will implement a script to automatically convert NASA GMAT .script files into equivalent Nyx Python scripts. By parsing the GMAT script into an abstract syntax tree, we can map each element to its Nyx Python API equivalent to generate a full Nyx script.

Requirements

  1. Write a GMAT script parser to construct an abstract syntax tree (AST) from a .script file. The parser should:
  • Handle all syntax in the GMAT scripting language
  • Parse comments, whitespace, etc. correctly
  • Detect syntax errors and report them with useful error messages
  1. Map each node in the GMAT AST to its equivalent Nyx Python API call. For example:
  • GMAT Variable -> Nyx Spacecraft
  • GMAT For Loop -> Python for loop
  • GMAT Propagate -> Nyx Propagator
  • GMAT ForceModel -> Nyx Orbital and Spacecraft dynamics
  1. Generate formatted Python code from the mapped AST (e.g. run ruff).

  2. Handle GMAT syntax/functions that do not have a direct Nyx equivalent. In some cases, manual intervention or editing of the output script may be required. Provide clear warnings where full automation was not possible. The idea here would be to store parallel errors and only crash once, showing all of the errors that failed.

  3. Provide a CLI to run the conversion script, passing a GMAT .script file as input and printing the Nyx Python code to a folder that contains the recommended skeleton for a Nyx project.

  4. Add tests to validate the correct conversion of various GMAT script examples and catch edge cases. Test for accurate Python syntax, Nyx API usage, and runtime correctness of the output scripts.

  5. Update documentation to describe the GMAT script conversion feature, include installation/usage of the CLI, and link to examples.

Test plans

Here are the test plans for the GMAT script conversion feature:

Test Plans

Unit tests

  • Test the GMAT script parser on small code snippets and assert the AST is constructed correctly.
  • Test mapping individual GMAT AST nodes to Nyx Python API calls.
  • Test generating Python code from a simple mapped AST.

End-to-end examples

  • Provide several full GMAT example scripts with known correct Nyx equivalents. Assert the CLI produces the expected Python scripts.
  • Examples should cover major syntax like variables, integration, events, reportCreation, etc.
  • Examples should also include scripts that cannot be fully automated to confirm useful warnings are given.

Edge cases

  • Test unsupported GMAT functions to ensure they are detected and warned appropriately.
  • Test invalid GMAT scripts to assert meaningful errors are reported.
  • Excessively long/complex GMAT scripts to catch potential recursion or memory issues.

CLI usage

  • Test calling the CLI with no arguments or an invalid file to confirm usage info is shown.
  • Test calling the CLI on a folder of examples to assert all scripts are converted.
  • Pipe the CLI output to a Python executable to actually run the generated Nyx scripts and assert the expected output/behavior.

Documentation

  • Ensure the GMAT conversion feature is clearly documented, including CLI usage and installation.
  • Check that links to example input/output scripts function correctly.
  • Instructions for handling non-automated parts of the conversion should be included.

Limitations

  • Complex GMAT scripts, especially those using graphical interfaces, may not convert fully or run the same in Nyx. Warn the user of potential limitations.
  • Nyx lacks some features of GMAT, so some loss of functionality in the conversion is inevitable. Document any major differences.

Future work

  • Explore taking mapping configurations again to allow more customization in the conversion. But in a controlled manner.
  • Build up a library of example GMAT/Nyx script pairs to expand test coverage.
  • Continue improving the conversion to handle more of GMAT's syntax automatically over time. But avoid scope creep!

Design

Abstract syntax tree are great. That's it really.

@ChristopherRabotin ChristopherRabotin added Status: Design Issue at Design phase of the quality assurance process Kind: New feature This is a proposed new feature Interface: Python Priority: normal labels May 2, 2023
@ChristopherRabotin
Copy link
Member Author

GPT-4's proposed solution:

To create an AST (Abstract Syntax Tree) parser for GMAT scripts and convert them to Python scripts that use Nyx, you'll need to follow these general steps:

Tokenize the GMAT script: Split the script into tokens (words, symbols, etc.) that can be processed individually.
Build the AST: Process the tokens to create an abstract syntax tree that represents the structure of the GMAT script.
Traverse the AST and generate the corresponding Nyx Python script.

Here's a simple example using Python's ply library (Python Lex-Yacc) to get you started:

First, install the ply library:

pip install ply

Next, create a file named gmat_parser.py with the following content:

import ply.lex as lex
import ply.yacc as yacc

# Token list
tokens = (
    'CREATE', 'SPACECRAFT', 'IDENTIFIER',
    'BEGINMISSIONSEQUENCE', 'ENDMISSIONSEQUENCE',
    'PROPAGATE', 'LPAREN', 'RPAREN', 'LBRACE', 'RBRACE',
    'SEMICOLON', 'EQUALS', 'PERIOD',
)

# Token definitions
t_CREATE = r'Create'
t_SPACECRAFT = r'Spacecraft'
t_IDENTIFIER = r'[a-zA-Z_][a-zA-Z0-9_]*'
t_BEGINMISSIONSEQUENCE = r'BeginMissionSequence'
t_ENDMISSIONSEQUENCE = r'EndMissionSequence'
t_PROPAGATE = r'Propagate'
t_LPAREN = r'\('
t_RPAREN = r'\)'
t_LBRACE = r'\{'
t_RBRACE = r'\}'
t_SEMICOLON = r';'
t_EQUALS = r'='
t_PERIOD = r'\.'

# Ignored characters
t_ignore = " \t"


# Lexer error handling
def t_error(t):
    print(f"Illegal character '{t.value[0]}'")
    t.lexer.skip(1)


# Build the lexer
lexer = lex.lex()


# Parsing rules
def p_script(p):
    '''script : objects mission_sequence'''
    p[0] = {'objects': p[1], 'mission_sequence': p[2]}


def p_objects(p):
    '''objects : create_object objects
               | empty'''
    if len(p) == 3:
        p[0] = [p[1]] + p[2]
    else:
        p[0] = []


def p_create_object(p):
    '''create_object : CREATE SPACECRAFT IDENTIFIER SEMICOLON'''
    p[0] = {'type': 'spacecraft', 'name': p[3]}


def p_mission_sequence(p):
    '''mission_sequence : BEGINMISSIONSEQUENCE statements ENDMISSIONSEQUENCE'''
    p[0] = p[2]


def p_statements(p):
    '''statements : statement SEMICOLON statements
                  | empty'''
    if len(p) == 4:
        p[0] = [p[1]] + p[3]
    else:
        p[0] = []


def p_statement(p):
    '''statement : propagate_statement'''
    p[0] = p[1]


def p_propagate_statement(p):
    '''propagate_statement : PROPAGATE IDENTIFIER LPAREN IDENTIFIER RPAREN LBRACE IDENTIFIER PERIOD IDENTIFIER EQUALS NUMBER RBRACE'''
    p[0] = {
        'type': 'propagate',
        'propagator': p[2],
        'spacecraft': p[4],
        'parameter': p[7] + '.' + p[9],
        'value': p[11],
    }

def p_NUMBER(p):
    '''NUMBER : FLOAT
              | INT'''
    p[0] = p[1]

def p_FLOAT(p):
    r'FLOAT : \d+\.\d+'
    p[0] = float(p[1])

def p_INT(p):
    r'INT : \d+'
    p[0] = int(p[1])

def p_empty(p):
    'empty :'
    pass

# Error rule for syntax errors
def p_error(p):
    print(f"Syntax error in input: {p}")

# Build the parser
parser = yacc.yacc()

# Test the parser
if __name__ == "__main__":
    test_gmat_script = """
    Create Spacecraft MySpacecraft;
    BeginMissionSequence
        Propagate MyPropagator(MySpacecraft) {MySpacecraft.ElapsedDays = 1};
    EndMissionSequence
    """

    ast = parser.parse(test_gmat_script)
    print(ast)

This example demonstrates a basic GMAT script parser using Python's ply library. It defines the lexer and parser rules for a limited set of GMAT script instructions, specifically focusing on the Create Spacecraft and Propagate instructions.

To create the corresponding Nyx Python script, you'll need to traverse the generated AST and write functions that output the relevant Nyx Python code for each GMAT instruction.

For instance, you could create a function gmat_ast_to_nyx(ast: dict) -> str that takes the AST generated by the parser and returns a string containing the corresponding Nyx Python script.

Keep in mind that this is a simple example and only covers a limited set of GMAT instructions. You'll need to expand the lexer and parser rules to handle a broader range of GMAT instructions to create a more comprehensive AST parser.

@ChristopherRabotin
Copy link
Member Author

GPT-4 when asked to use Lark instead (seems to be recommended).

Sure! Here's the same GMAT script parser implemented using Lark. First, install the lark-parser library:

pip install lark-parser

Next, create a file named gmat_parser_lark.py with the following content:

from lark import Lark, Transformer

grammar = """
    start: objects mission_sequence

    objects: create_object*
    create_object: "Create" "Spacecraft" IDENTIFIER ";"

    mission_sequence: "BeginMissionSequence" statement* "EndMissionSequence"
    statement: propagate_statement ";"

    propagate_statement: "Propagate" IDENTIFIER "(" IDENTIFIER ")" "{" IDENTIFIER "." IDENTIFIER "=" NUMBER "}"
    
    NUMBER: FLOAT | INT
    FLOAT: /\d+\.\d+/
    INT: /\d+/
    
    IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/

    %import common.WS
    %ignore WS
"""

class GmatTransformer(Transformer):
    def start(self, items):
        return {'objects': items[0], 'mission_sequence': items[1]}

    def objects(self, items):
        return items

    def create_object(self, items):
        return {'type': 'spacecraft', 'name': items[0]}

    def mission_sequence(self, items):
        return items

    def statement(self, items):
        return items[0]

    def propagate_statement(self, items):
        return {
            'type': 'propagate',
            'propagator': items[0],
            'spacecraft': items[1],
            'parameter': items[2] + '.' + items[3],
            'value': items[4],
        }

    def NUMBER(self, token):
        try:
            return int(token)
        except ValueError:
            return float(token)

    def IDENTIFIER(self, token):
        return token[0]

parser = Lark(grammar, parser="lalr", transformer=GmatTransformer())

if __name__ == "__main__":
    test_gmat_script = """
    Create Spacecraft MySpacecraft;
    BeginMissionSequence
        Propagate MyPropagator(MySpacecraft) {MySpacecraft.ElapsedDays = 1};
    EndMissionSequence
    """

    ast = parser.parse(test_gmat_script)
    print(ast)

This implementation defines the grammar using Lark's EBNF-like syntax and uses a Transformer class to transform the parsed tree into an AST. The syntax for defining the grammar is more concise and expressive, and the generated AST will be similar to the one produced by the PLY-based implementation.

As with the PLY example, you will need to traverse the generated AST and write functions that output the relevant Nyx Python code for each GMAT instruction to convert the GMAT script to a Nyx Python script.

(Later)

Here's a function named gmat_ast_to_nyx() that takes the Lark AST generated by the parser and returns a string containing the corresponding Nyx Python script. This function assumes that the GMAT script contains only the Create Spacecraft and Propagate instructions, as shown in the previous examples. You will need to expand this function to handle other GMAT instructions as needed.

def gmat_ast_to_nyx(ast: dict) -> str:
    # Initialize an empty list to store lines of the Nyx script
    nyx_lines = []

    # Handle objects (Spacecraft)
    for obj in ast['objects']:
        if obj['type'] == 'spacecraft':
            nyx_lines.append(f"# Create Spacecraft {obj['name']}")
            nyx_lines.append(f"sc = Spacecraft()  # Replace with proper Spacecraft initialization")

    # Handle mission sequence
    for stmt in ast['mission_sequence']:
        if stmt['type'] == 'propagate':
            nyx_lines.append(f"# Propagate {stmt['propagator']}({stmt['spacecraft']}) {{ {stmt['parameter']} = {stmt['value']} }}")
            nyx_lines.append(f"dynamics = SpacecraftDynamics()  # Replace with proper SpacecraftDynamics initialization")
            nyx_lines.append(f"duration = Duration(Unit.Day * {stmt['value']})")
            nyx_lines.append(f"rslt, traj = propagate(sc, dynamics, duration)")

    # Return the Nyx script as a single string
    return '\n'.join(nyx_lines)

You can use this function in conjunction with the Lark parser to convert a GMAT script into a Nyx Python script. Here's an example of how to do that:

# Parse the GMAT script using the Lark parser
ast = parser.parse(test_gmat_script)

# Convert the GMAT AST to a Nyx Python script
nyx_script = gmat_ast_to_nyx(ast)

# Print the Nyx script
print(nyx_script)

Please note that this function only provides a starting point for the conversion and includes placeholders for initializing the Spacecraft and SpacecraftDynamics objects. You will need to replace these placeholders with the appropriate initialization code based on the information available in your GMAT script or external configuration files.

As you expand your GMAT parser to handle more instructions, you will also need to update the gmat_ast_to_nyx() function to generate the corresponding Nyx Python code for each new instruction.

@ChristopherRabotin
Copy link
Member Author

GPT-4

I will show you how to parse the GMAT ForceModel and Propagator definitions using Lark and convert them into the corresponding Nyx YAML format.

First, let's create a Lark grammar to parse the given GMAT script:

gmat_force_model_grammar = r"""
start: force_model* propagator*

force_model: "Create ForceModel" NAME force_model_item+
force_model_item: NAME "." NAME "=" (NAME | "{" NAME ("," NAME)* "}" | "On" | "None" | NUMBER)

propagator: "Create Propagator" NAME propagator_item+
propagator_item: NAME "." NAME "=" (NAME | NUMBER)

%import common.CNAME -> NAME
%import common.NUMBER
%import common.WS
%ignore WS
"""

parser = Lark(gmat_force_model_grammar, start="start", parser="lalr")

Now we can parse the GMAT script and extract the ForceModel and Propagator information:

gmat_script = """
%----------------------------------------------------
% Configure propagators and force models
%----------------------------------------------------

...  % Your GMAT script content here
"""

ast = parser.parse(gmat_script)

Next, let's create a function named gmat_force_models_and_propagators_to_nyx_yaml() that takes the parsed Lark AST and converts it into the corresponding Nyx YAML format:

def gmat_force_models_and_propagators_to_nyx_yaml(ast: lark.Tree) -> str:
    force_models = {}
    
    for item in ast.children:
        if item.data == "force_model":
            force_model = {}
            name = item.children[0]
            
            for attr in item.children[1:]:
                key = attr.children[0] + "." + attr.children[1]
                value = attr.children[2]
                force_model[key] = value
                
            force_models[name] = force_model

    # Convert GMAT force models to Nyx format
    nyx_force_models = {}
    
    for name, model in force_models.items():
        nyx_model = {}

        point_masses = []
        for key, value in model.items():
            if key.endswith("PointMasses"):
                point_masses.extend(value.children)

        if point_masses:
            nyx_model['point_masses'] = point_masses

        if "GravityField" in name:
            nyx_model['harmonics'] = [{
                'frame': model.get('GravityField.Earth.Frame', 'IAU Earth'),
                'coeffs': model.get('GravityField.Earth.Coeffs', 'data/JGM3.cof.gz'),
                'degree': model.get('GravityField.Earth.Degree', 10),
                'order': model.get('GravityField.Earth.Order', 10),
            }]

        if "SRP" in name:
            nyx_model['srp'] = {
                'phi': 1367.0,  # Default value, you can customize it as needed
                'shadows': point_masses,
            }

        nyx_force_models[name] = nyx_model

    return yaml.dump(nyx_force_models)

You can use this function to convert the parsed Lark AST of the GMAT script to a Nyx YAML string:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Interface: Python Kind: New feature This is a proposed new feature Priority: normal Status: Design Issue at Design phase of the quality assurance process Topic: Mission Design
Projects
None yet
Development

No branches or pull requests

1 participant