#=============================================================================
#
#  Copyright (c) Kitware, Inc.
#  All rights reserved.
#  See LICENSE.txt for details.
#
#  This software is distributed WITHOUT ANY WARRANTY; without even
#  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
#  PURPOSE.  See the above copyright notice for more information.
#
#=============================================================================
"""
Export script for ACE3P workflows
"""
import datetime
import logging
import os
import sys
sys.dont_write_bytecode = True

import smtk
if 'pybind11' == smtk.wrappingProtocol():
    #print 'Using pybind11 bindings'
    import smtk.attribute
    import smtk.model
    import smtk.simulation

from internal.writers import utils, loading
from internal.writers import tem3pwriter  # for testing only

ExportScope = type('ExportScope', (object,), dict())
# ---------------------------------------------------------------------
def ExportCMB(spec):
    '''Entry function, called by CMB to write export files

    Returns boolean indicating success
    Parameters
    ----------
    spec: Top-level object passed in from CMB
    '''
    #print 'Enter ExportCMB()'

    # Initialize scope instance to store spec values and other info
    scope = ExportScope()
    scope.logger = spec.getLogger()
    scope.sim_atts = spec.getSimulationAttributes()
    if scope.sim_atts is None:
        msg = 'ERROR - No simlation attributes'
        print msg
        raise Exception(msg)

    reload(utils)
    utils.get_model_info(scope)
    print 'scope.model_path %s' % scope.model_path

    scope.export_atts = spec.getExportAttributes()
    if scope.export_atts is not None:
        att_list = scope.export_atts.findAttributes('ExportSpec')
    if len(att_list) > 1:
        msg = 'More than one ExportSpec instance -- ignoring all'
        print 'WARNING:', msg
        scope.logger.addWarning(msg)
        return False

    # (else)
    export_spec_att = att_list[0]

    # Initialize solver list
    solver_item = export_spec_att.findString('Analysis')
    if solver_item is None:
        msg = 'Missing \"Analysis\" item -- cannot export'
        scope.logger.addError(msg)
        raise Exception(msg)
    if not solver_item.isSet(0):
        msg = 'Analysis item is not set -- cannot export'
        scope.logger.addError(msg)
        raise Exception(msg)

    solver_string = solver_item.value(0)
    print 'solver_string:', solver_string
    scope.solver_list = solver_string.split('.')
    scope.domain_source = None
    if scope.solver_list[0] == 'track3p':
        # Special case - check Domain for prerequisite simulation
        att_list = scope.sim_atts.findAttributes('Domain')
        if not att_list:
            msg = 'Track3P simulation missing Domain attribute'
        domain_att = att_list[0]
        input_data_item = domain_att.findGroup('InputData')
        source_item = input_data_item.find('Source')
        if source_item.value(0) == 'Omega3P':
            scope.solver_list = ['omega3p'] + scope.solver_list
        elif source_item.value(0) == 'S3P':
            scope.solver_list = ['s3p'] + scope.solver_list
        # And save for later
        scope.domain_source = source_item.value(0)

    folder_item = export_spec_att.findDirectory('OutputFolder')
    if folder_item is None or not folder_item.isSet(0):
        msg = 'Output folder not set -- cannot export'
        scope.logger.addError(msg)
        raise Exception(msg)
    scope.output_folder = folder_item.value(0)

    file_prefix_item = export_spec_att.findString('OutputFilePrefix')
    if file_prefix_item is None or not file_prefix_item.isSet(0):
        msg = 'Output file prefix not set -- cannot export'
        scope.logger.addError(msg)
        raise Exception(msg)
    scope.output_file_prefix = file_prefix_item.value(0)

    # Keep track of files & folders that need to get uploaded
    # Use a set to prevent inserting that same thing twice
    scope.files_to_upload = set()
    scope.files_to_upload.add(scope.model_path)  # always
    scope.folders_to_upload = set()
    scope.symlink = None  # for track3p case

    # Need a map of solver string to full analysis name
    analysis_lookup = {
        'omega3p': 'Omega3P Analysis',
        'o3p': 'Omega3P Analysis',
        's3p': 'S3P Analysis',
        't3p': 'T3P Analysis',
        'track3p': 'Track3P Analysis',
        'tem3p-eigen': 'TEM3P Structural Eigenmode',
        'tem3p-structure': 'TEM3P Structural Static',
        'tem3p-harmonic': 'TEM3P Harmonic Response',
        'tem3p-thermal-linear': 'TEM3P Thermal Linear',
        'tem3p-thermal-nonlinear': 'TEM3P Thermal Nonlinear'
    }

    # Loop over all solvers and write corresponding files
    for solver in scope.solver_list:
        # Hack/workaround for typo in export template
        if solver == 'temp3p-thermal-nonlinear':
            solver = 'tem3p-thermal-nonlinear'

        # Get full analysis name
        analysis = analysis_lookup.get(solver)
        if not analysis:
            msg = 'The ACE3P analysis is not specified, solver %s' % solver
            scope.logger.addError(msg)
            raise Exception(msg)
        print 'Writing %s' % analysis

        # Get categories
        categories = scope.sim_atts.analysisCategories(analysis)
        scope.categories = list(categories)
        print 'Using categories: %s' % scope.categories

        # Initialize output file
        extension_lookup = {
            'tem3p-eigen': 'tem3p',
            'tem3p-structure': 'tem3p',
            'tem3p-harmonic': 'tem3p',
            'tem3p-thermal-linear': 'tem3p',
            'tem3p-thermal-nonlinear': 'tem3p'
        }
        file_ext = extension_lookup.get(solver, solver)
        filename = '%s.%s' % (scope.output_file_prefix, file_ext)
        output_path = os.path.join(scope.output_folder, filename)
        print 'Output file %s' % output_path

        completed = False
        with open(output_path, 'w') as scope.output:
            dt_string = datetime.datetime.now().strftime('%d-%b-%Y  %H:%M')
            line = '// Generated by CMB %s\n' % dt_string
            scope.output.write(line)

            if solver.startswith('tem3p'):
                from internal.writers import tem3pwriter
                reload(tem3pwriter)
                writer = tem3pwriter.Tem3PWriter(solver)
                writer.write(scope)
            elif solver == 'track3p':
                # Track3P has a separate writer class
                from internal.writers import track3pwriter
                reload(track3pwriter)
                writer = track3pwriter.Track3PWriter()
                writer.write(scope)
            else:
                write_modelinfo(scope)
                write_finiteelement(scope)
                write_pregion(scope)
                if solver in ['omega3p', 'o3p']:
                    write_eigensolver(scope)
                elif solver == 's3p':
                    write_frequency_scan(scope)
                elif solver == 't3p':
                    write_moving_window(scope)
                    write_loading_info(scope)
                    reload(loading)
                    loading.write_loading(scope)
                    write_time_stepping(scope)
                    write_monitor(scope)
                    write_linear_solver(scope)
                write_port(scope)
                write_postprocess(scope)
            print 'Wrote output file %s' % output_path
            completed = True
            scope.files_to_upload.add(output_path)

    print 'Export completion status: %s' % completed
    sys.stdout.flush()
    if not completed:
        return completed

    # (else)
    # Check for NERSCSimulation item
    sim_item = export_spec_att.find('NERSCSimulation')
    if sim_item is not None and sim_item.isEnabled():
        # Import nersc module (only when needed)
        from internal.writers import nersc
        reload(nersc)

        completed = nersc.submit_ace3p(scope, sim_item)
        print 'Submit to NERSC status: %s' % completed
    else:
        # For test/debug, list any files to upload
        print 'Files for upload:'
        for filename in scope.files_to_upload:
            print '  ', filename
        # And any sym link
        print 'Symlink', scope.symlink


    return completed

# ---------------------------------------------------------------------
def write_modelinfo(scope):
    '''Writes ModelInfo section to output stream

    Model info should have already been added to scope object
    '''
    scope.output.write('ModelInfo:\n')
    scope.output.write('{\n')

    # Always write model with ncdf extension
    # (If input is .gen file, must convert using acdtool)
    root, ext = os.path.splitext(scope.model_file)
    ncdf_model_file = root + '.ncdf'
    logging.info('ncdf model_file %s' % ncdf_model_file)
    scope.output.write('  File: %s\n' % ncdf_model_file)

    write_boolean(scope, 'Tolerant')
    scope.output.write('\n')
    write_boundarycondition(scope)
    write_materials(scope)

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_boundarycondition(scope):
    '''Writes SurfaceProperty attributes to output stream

    '''
    print 'Write boundary conditions'
    atts = scope.sim_atts.findAttributes('SurfaceProperty')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    name_list = [
        'Electric', 'Magnetic', 'Exterior', 'Impedance', 'Absorbing',
        'Waveguide', 'Periodic']

    scope.output.write('  BoundaryCondition: {\n')

    # First write HFormulation if item is enabled
    write_boolean(scope, 'HFormulation', indent='    ')

    # Traverse attributes and write BoundaryCondition contents
    surface_material_list = list()  # for saving SurfaceMaterial info
    for att in atts:
        ent_string = utils.format_entity_string(scope, att)
        if not ent_string:
            continue  # warning?

        att_type = att.type()

        # Periodic BC is special case
        if att_type == 'Periodic':
            if scope.analysis_types[0] == 'S3P':
                raise Exception('ERROR: Specified Periodic surface with S3P analysis -- invalid')

            # Write slave item
            scope.output.write('    Periodic_S: %s\n' % ent_string)

            # Write master item
            slave_item = att.findModelEntity('MasterSurface')
            ent_ref = slave_item.value(0)
            if ent_ref:
                ent = ent_ref.entity()
                prop_idlist = scope.model_manager.integerProperty(ent, 'pedigree id')
                if prop_idlist:
                    scope.output.write('    Periodic_M: %s\n' % prop_idlist[0])
            else:
                print 'WARNING: No slave surface specified for Periodic BC'

            # Write relative phase angle
            phase_item = att.findDouble('Theta')
            phase = phase_item.value(0)
            scope.output.write('    Theta: %f\n' % phase)

            # This completes Periodic case
            continue

        scope.output.write('    %s: %s\n' % (att_type, ent_string))

        # Check for sigma and frequency items
        added_text = None
        sigma_item = att.findDouble('Sigma')
        if sigma_item is not None and sigma_item.isMemberOf(scope.categories):
            sigma = sigma_item.value(0)
            line1 = '    ReferenceNumber: %s\n' % ent_string
            line2 = '    Sigma: %g\n' % sigma
            added_text = line1 + line2

            # Frequency only set when Sigma is set (T3P Impedance)
            freq_item = att.findDouble('Frequency')
            if freq_item is not None and freq_item.isMemberOf(scope.categories):
                freq = freq_item.value(0)
                line3 = '    Frequency: %d\n' % freq
                added_text += line3

        if added_text:
            surface_material_list.append(added_text)
    scope.output.write('  }\n')

    # Traverse surface_material_list and write SurfaceMaterial entries
    for surface_material_string in surface_material_list:
        scope.output.write('\n')
        scope.output.write('  SurfaceMaterial: {\n')
        scope.output.write(surface_material_string)
        scope.output.write('  }\n')

# ---------------------------------------------------------------------
def write_boolean(scope, att_type, item_name=None, output_name=None, indent='  '):
    '''Writes boolean property if item is checked

    Attribute should be a singleton
    '''
    #print 'write_boolean', att_type
    atts = scope.sim_atts.findAttributes(att_type)
    if not atts:
        return

    # (else)
    att = atts[0]
    if not item_name:
        item_name = att_type
    item = att.findVoid(item_name)

    if not output_name:
        output_name = att_type
    if item and item.isEnabled():
        scope.output.write('%s%s: 1\n' % (indent, output_name))

# ---------------------------------------------------------------------
def write_materials(scope):
    '''Writes Material attributes to output stream

    '''
    print 'Write materials'
    atts = scope.sim_atts.findAttributes('Material')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    # Traverse attributes
    for att in atts:
        ent_string = utils.format_entity_string(scope, att)
        if not ent_string:
            continue  # warning?

        scope.output.write('\n')
        scope.output.write('  Material: {\n')
        scope.output.write('    Attribute: %s\n' % ent_string)

        # Make list of (item name, output label) to write
        items_todo = [
            ('Epsilon', 'Epsilon'),
            ('Mu', 'Mu'),
            ('ImgEpsilon', 'EpsilonImag'),
            ('ImgMu', 'MuImag')
        ]
        for item_info in items_todo:
            name, label = item_info
            item = att.findDouble(name)
            if item and item.isEnabled() and item.isMemberOf(scope.categories):
                value = item.value(0)
                scope.output.write('    %s: %g\n' % (label, value))

        scope.output.write('  }\n')


# ---------------------------------------------------------------------
def write_finiteelement(scope):
    '''Writes FiniteElement section to output stream

    '''
    print 'Write FiniteElement'
    scope.output.write('\n')
    scope.output.write('FiniteElement:\n')
    scope.output.write('{\n')

    att = scope.sim_atts.findAttributes('FEInfo')[0]

    order_item = att.findInt('Order')
    scope.output.write('  Order: %d\n' % order_item.value(0))

    curved_surfaces = 'off'
    curved_item = att.findVoid('EnableCurvedSurfaces')
    if curved_item and curved_item.isEnabled():
        curved_surfaces = 'on'
    scope.output.write('  CurvedSurfaces: %s\n' % curved_surfaces)

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_pregion(scope):
    '''Writes PRegion section to output stream

    '''
    print 'Write PRegion'
    atts = scope.sim_atts.findAttributes('RegionHighOrder')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    # Traverse attributes
    for att in atts:
        ent_string = utils.format_entity_string(scope, att)
        if not ent_string:
            continue  # warning?

        order_item = att.findInt('RegionHighOrder')
        order = order_item.value(0)

        scope.output.write('\n')
        scope.output.write('PRegion:\n')
        scope.output.write('{\n')
        scope.output.write('  Type: Material\n')
        scope.output.write('  Reference: %s\n' % ent_string)
        scope.output.write('  Order: %d\n' % order)
        scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_eigensolver(scope):
    '''Writes Omega3P EigenSolver section to output stream

    '''
    print 'Write EigenSolver'
    att = scope.sim_atts.findAttributes('FrequencyInfo')[0]
    scope.output.write('\n')
    scope.output.write('EigenSolver:\n')
    scope.output.write('{\n')

    num_item = att.findInt('NumEigenvalues')
    scope.output.write('  NumEigenvalues: %d\n' % num_item.value(0))
    freq_item = att.findDouble('FrequencyShift')
    scope.output.write('  FrequencyShift: %g\n' % freq_item.value(0))

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_frequency_scan(scope):
    '''Writes S3P FrequencyScan section

    '''
    print 'Write FrequencyScan'
    att = scope.sim_atts.findAttributes('FrequencyInfo')[0]
    scope.output.write('\n')
    scope.output.write('FrequencyScan:\n')
    scope.output.write('{\n')

    # Make list of (smtk_item_name, s3p_file_keyword)
    item_spec = [
        ('StartingFrequency', 'Start'),
        ('EndingFrequency', 'End'),
        ('FrequencyInterval','Interval')]
    for item_name,s3p_keyword in item_spec:
        item = att.findDouble(item_name)
        scope.output.write('  %s: %g\n' % (s3p_keyword, item.value(0)))

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_moving_window(scope):
    '''Writes PRegion w/AutomaticMovingWindow for wakefield analyses

    '''
    att_list = scope.sim_atts.findAttributes('MovingWindow')
    if not att_list:  # older version
        return

    att = att_list[0]
    group_item = att.findGroup('MovingWindow')
    if group_item is None or not group_item.isEnabled():
        return

    print 'Write AutomaticMovingWindow'
    scope.output.write('\n')
    scope.output.write('PRegion:\n')
    scope.output.write('{\n')

    scope.output.write('  Type: AutomaticMovingWindow\n')
    order_item = group_item.find('Order')
    scope.output.write('  Order: %d\n' % order_item.value(0))
    back_item = group_item.find('Back')
    scope.output.write('  Back: %s\n' % back_item.value(0))
    front_item = group_item.find('Front')
    scope.output.write('  Front: %s\n' % front_item.value(0))
    end_item = group_item.find('StructureEnd')
    scope.output.write('  StructureEnd: %s\n' % end_item.value(0))

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_loading_info(scope):
    '''Writes LoadingInfo sections for T3P analysis

    '''
    att_list = scope.sim_atts.findAttributes('BeamLoading')
    if not att_list:
        return

    print 'Write LoadingInfo'
    for att in att_list:
        scope.output.write('\n')
        scope.output.write('LoadingInfo:\n')
        scope.output.write('{\n')

        # Write the Bunch subsection
        scope.output.write('\n')
        scope.output.write('  Bunch:\n')
        scope.output.write('  {\n')

        bunch_type = att.findString('Bunch Type').value(0)
        if bunch_type == 'Gaussian':
            scope.output.write('  Type: Gaussian\n')

            sigma = att.findDouble('Sigma').value(0)
            scope.output.write('  Sigma: %f\n' % sigma)
        elif bunch_type == 'Bi-Gaussian':
            scope.output.write('  Type: BiGaussian\n')

            sigma = att.findDouble('Sigma1').value(0)
            scope.output.write('  Sigma1: %f\n' % sigma)

            sigma = att.findDouble('Sigma2').value(0)
            scope.output.write('  Sigma2: %f\n' % sigma)
        else:
            raise Exception('Unrecognized Bunch Type %s' % bunch_type)

        # Common Bunch parameters
        num_sigmas = att.findInt('numSigmas').value(0)
        scope.output.write('  Number of sigmas: %d\n' % num_sigmas)

        charge = att.findDouble('Charge').value(0)
        scope.output.write('  Charge: %g\n' % charge)

        # Finish Bunch subsection
        scope.output.write('  }\n')

        # Rest of LoadingInfo

        # SymmetryFactor
        symmetry_factor = att.findDouble('SymmetryFactor').value(0)
        scope.output.write('  SymmetryFactor: %s\n' % symmetry_factor)

        # Start point
        start_point_item = att.findDouble('StartPoint')
        start_point_string = utils.format_vector(start_point_item, '%g')
        scope.output.write('  StartPoint: %s\n' % start_point_string)

        # Todo Direction
        direction_item = att.findDouble('Direction')
        direction_string = utils.format_vector(direction_item, '%g')
        scope.output.write('  Direction: %s\n' % direction_string)

        # Source boundary
        boundary_item = att.findModelEntity('SourceBoundary')
        boundary_ids = utils.get_entity_ids(scope, boundary_item)
        if len(boundary_ids) == 0:
            raise Exception('No SourceBoundary specified for LoadingInfo %s' % att.name())
        scope.output.write('  BoundaryID: %d\n' % boundary_ids[0])

        # Finish LoadingInfo
        scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_time_stepping(scope):
    '''Writes TimeStepping section for T3P analysis

    '''
    print 'Write TimeStepping'

    att_list = scope.sim_atts.findAttributes('FEInfo')
    if not att_list:
        print 'Missing FEInfo attribute'
        return
    feinfo_att = att_list[0]

    scope.output.write('\n')
    scope.output.write('TimeStepping:\n')
    scope.output.write('{\n')

    max_time = feinfo_att.findDouble('MaxTime').value(0)
    scope.output.write('  MaximumTime: %g\n' % max_time)

    dt = feinfo_att.findDouble('DT').value(0)
    scope.output.write('  DT: %g\n' % dt)

    scope.output.write('}\n')


# ---------------------------------------------------------------------
def write_monitor(scope):
    '''Writes Monitor sections for T3P analysis

    '''
    print 'Write Monitor'
    att_list = scope.sim_atts.findAttributes('Monitor')
    if not att_list:
        return

    # List of <item name, T3P keyword> pairs
    item_keyword_table = [
        ('Point', 'Point'),

        ('timeStart', 'TimeStart'),
        ('timeEnd', 'TimeEnd'),
        ('TimeStep', 'TimeStep'),

        ('startContour', 'StartContour'),
        ('endContour', 'EndContour'),
        ('Smax', 'Smax')
    ]

    # Map attribute type to T3P keyword
    type_map = {
        'ModeVoltageMonitor': 'ModeVoltage',
        'PointMonitor': 'Point',
        'PowerMonitor': 'Power',
        'VolumeMonitor': 'Volume',
        'WakefieldMonitor': 'Wakefield'
    }

    for att in att_list:
        scope.output.write('\n')
        scope.output.write('Monitor:\n')
        scope.output.write('{\n')

        type_string = type_map.get(att.type())
        if type_string is None:
            logging.warning('Unknown monitor type %s' % att.type())
            type_string = att.type()
        scope.output.write('  Type: %s\n' % type_string)
        # Name current found as name item instead of att.name()
        name_item = att.findString('Name')
        scope.output.write('  Name: %s\n' % name_item.value(0))

        # Voltage Monitor a special case
        if type_string == 'ModeVoltage':
            write_mode_voltage_monitor(scope, att)
            scope.output.write('}\n')
            continue

        for item_type, keyword in item_keyword_table:
            item = att.find(item_type)
            if item is None or not item.isMemberOf(scope.categories):
                continue
            if item.numberOfValues() > 1:
                value_string = utils.format_vector(item)
                scope.output.write('  %s: %s\n' % (keyword, value_string))
            else:
                scope.output.write('  %s: %s\n' % (keyword, item.value(0)))

        # Handle MonitorBoundary as special case
        boundary_item = att.findModelEntity('MonitorBoundary')
        if boundary_item is not None:
            idlist = utils.get_entity_ids(scope, boundary_item)
            scope.output.write('  ReferenceNumber: %d\n' % idlist[0])
        scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_mode_voltage_monitor(scope, att)        :
    '''Writes unique parts of Voltage Monitor section for T3P analysis

    * Only writes inner part of the section.
    * Type and Name should be written by calling code
    '''
    scope.output.write('  Port: {\n')

    model_entity_item = att.find('MonitorPort')
    ent_idlist = utils.get_entity_ids(scope, model_entity_item)
    if not ent_idlist:
        raise Exception('ModeVoltage specified without Port')
    scope.output.write('    ReferenceNumber: %d\n' % ent_idlist[0])

    group_item = att.find('ESolver')
    scope.output.write('    ESolver: {\n')

    # List of <item name, T3P keyword> pairs
    item_keyword_table = [
        ('Type', 'Type', '%s'),
        ('NumberOfModes', 'NumberOfModes', '%d'),
        ('Frequency', 'Frequency', '%g'),
        ('Tolerance', 'Tolerance', '%g'),
        ('MaxIterations', 'MaxIterations', '%d')
    ]
    for item_type,keyword,fmt in item_keyword_table:
        item = group_item.find(item_type)
        #scope.output.write('      %s: %s\n' % (keyword, item.value(0)))
        format_string = '      %s: %s\n' % (keyword, fmt)
        scope.output.write(format_string % item.value(0))
    scope.output.write('    }\n')

    scope.output.write('  }\n')

# ---------------------------------------------------------------------
def write_linear_solver(scope):
    '''Writes LinearSolver section for T3P analysis

    '''
    print 'Write LinearSolver'

    att_list = scope.sim_atts.findAttributes('FEInfo')
    if not att_list:
        print 'Missing FEInfo attribute'
        return
    feinfo_att = att_list[0]

    scope.output.write('\n')
    scope.output.write('LinearSolver:\n')
    scope.output.write('{\n')

    solver_item = feinfo_att.findString('LinearSolver')
    scope.output.write('  Solver: %s\n' % solver_item.value(0))

    # Check for optional preconditioner
    precondx_item = solver_item.findChild(
        'Preconditioner', smtk.attribute.ACTIVE_CHILDREN)
    if precondx_item is not None:
        scope.output.write('  Preconditioner: %s\n' % precondx_item.value(0))

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_port(scope):
    '''Writes Port sections for each surface of att_type Waveguide

    '''
    print 'Write Port'
    atts = scope.sim_atts.findAttributes('Waveguide')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    # Traverse attributes
    for att in atts:
        #print 'att', att.name()

        ent_idlist = utils.get_entity_ids(scope, att.associations())
        if not ent_idlist:
            print 'WARNING: Waveguide %s not assigned to any model entities' % att.name()
            continue

        # Can only specify one ReferenceNumber per Port,
        # so write separate Port section for each model entity
        for ent_id in ent_idlist:
            mode_item = att.findInt('NumModes')
            num_modes = mode_item.value(0)

            scope.output.write('\n')
            scope.output.write('Port:\n')
            scope.output.write('{\n')
            scope.output.write('  ReferenceNumber: %s\n' % ent_id)
            scope.output.write('  NumberOfModes: %d\n' % num_modes)
            scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_postprocess(scope):
    '''Writes PostProcess section to output stream

    '''
    att_list = scope.sim_atts.findAttributes('PostProcess')
    if not att_list:
        return

    att = att_list[0]

    # Not all solvers use PostProcess
    if not att.isMemberOf(scope.categories):
        return

    print 'Write PostProcess'
    scope.output.write('\n')
    scope.output.write('PostProcess:\n')
    scope.output.write('{\n')

    # Version 0
    item = att.find('ModeFiles')
    if item:
        toggle = 'on' if item.isEnabled() else 'off'
        scope.output.write('  Toggle: %s\n' % toggle)

    # Version 1 and up
    toggle_item = att.findGroup('Toggle')
    if toggle_item:
        toggle = 'on' if toggle_item.isEnabled() else 'off'
        scope.output.write('  Toggle: %s\n' % toggle)

        prefix_item = toggle_item.find('ModeFilePrefix')
        if prefix_item.isEnabled() and prefix_item.isSet(0):
            prefix = prefix_item.value(0)
            scope.output.write('  ModeFile: %s\n' % prefix)

        # Write port numbers assigned to post processing
        ports_item = toggle_item.find('ports')
        for i in range(ports_item.numberOfGroups()):
            ref_item = ports_item.item(i, 0)
            ref_att = ref_item.value(0)
            ent_string = utils.format_entity_string(scope, ref_att)
            scope.output.write('  PortNumber: %s\n' % ent_string)

    scope.output.write('}\n')
