
from abaqus import mdb
from abaqusConstants import OFF
import os as _sw_os
_sw_before_jobs = {str(n): mdb.jobs[n] for n in mdb.jobs.keys()}
_sw_written_jobs = []
def _sw_record_write(_job, *args, **kwargs):
    _answer = _job.writeInput(*args, **kwargs)
    _name = str(getattr(_job, 'name', ''))
    if not _name or _name not in mdb.jobs.keys():
        _names = [str(n) for n in mdb.jobs.keys() if mdb.jobs[n] is _job]
        if len(_names) != 1:
            raise RuntimeError('Cannot identify the job which wrote the INP')
        _name = _names[0]
    _sw_written_jobs.append((_name, _sw_os.path.abspath(_name + '.inp')))
    return _answer

from abaqus import *
from abaqusConstants import *
import os
SOURCE = 'CCA_Cutting'

def unique_name(base, repository):
    name = base
    index = 1
    while name in repository:
        name = base + '_' + str(index)
        index += 1
    return name

def keyword_name(block):
    for line in block.splitlines():
        line = line.strip()
        if line and (not line.startswith('**')):
            return line.split(',')[0].strip().upper()
    return ''

def data_lines(block):
    result = []
    found = False
    for line in block.splitlines():
        stripped = line.strip()
        if stripped.startswith('**') or not stripped:
            continue
        if stripped.startswith('*'):
            found = True
            continue
        if found:
            result.append(stripped)
    return result
if SOURCE not in mdb.models:
    raise RuntimeError('Open the CAE database containing CCA_Cutting before running.')
source = mdb.models[SOURCE]
step_names = [name for name in source.steps.keys() if name != 'Initial']
if not step_names:
    raise RuntimeError('CCA_Cutting has no analysis step.')
for name in step_names:
    procedure = str(source.steps[name].procedureType).upper()
    if 'EXPLICIT' not in procedure:
        raise RuntimeError('This script requires Abaqus/Explicit steps: ' + name)
model_name = unique_name(SOURCE + '_Checked', mdb.models)
model = mdb.Model(name=model_name, objectToCopy=source)
assembly = model.rootAssembly
report = ['SOURCE_MODEL=' + SOURCE, 'CHECKED_MODEL=' + model_name, 'RESULT_PENETRATION=NOT_EVALUATED', 'Existing geometry, materials, damage, loads and ties are copied.', 'Existing meshes are retained.']
meshed_parts = set()
for instance_name in list(assembly.instances.keys()):
    instance = assembly.instances[instance_name]
    part = model.parts[instance.partName]
    if part.type == ANALYTIC_RIGID_SURFACE:
        continue
    if len(instance.elements):
        continue
    if instance.dependent == ON:
        if part.name not in meshed_parts:
            if not len(part.elements):
                part.generateMesh()
            meshed_parts.add(part.name)
    else:
        assembly.generateMesh(regions=(instance,))
assembly.regenerate()
solid_sets = []
for instance_name in list(assembly.instances.keys()):
    instance = assembly.instances[instance_name]
    part = model.parts[instance.partName]
    if part.type != DEFORMABLE_BODY:
        continue
    if not len(instance.elements):
        raise RuntimeError('Unmeshed deformable instance: ' + instance_name)
    labels = [element.label for element in instance.elements if str(element.type).upper() == 'C3D8R']
    report.append('INSTANCE=%s ELEMENTS=%d C3D8R=%d' % (instance_name, len(instance.elements), len(labels)))
    if not labels:
        continue
    set_name = unique_name('CCA_CHECK_SOLID', assembly.sets)
    elements = instance.elements.sequenceFromLabels(labels=tuple(labels))
    assembly.Set(name=set_name, elements=elements)
    solid_sets.append(set_name)
if not solid_sets:
    raise RuntimeError('No deformable C3D8R elements were found.')
model.keywordBlock.synchVersions(storeNodesAndElements=False)
original_blocks = tuple(model.keywordBlock.sieBlocks)
general_count = 0
exclusion_count = 0
all_exterior = False
explicit_self_pairs = []
for block in original_blocks:
    key = keyword_name(block)
    if key == '*CONTACT':
        general_count += 1
    if key == '*CONTACT EXCLUSIONS':
        exclusion_count += 1
    if key == '*CONTACT INCLUSIONS':
        if 'ALL EXTERIOR' in block.upper():
            all_exterior = True
        for line in data_lines(block):
            fields = [item.strip() for item in line.split(',')]
            if len(fields) >= 2:
                (first, second) = fields[:2]
                if not second or first.upper() == second.upper():
                    explicit_self_pairs.append(line)
    if key in ('*CONTACT', '*CONTACT INCLUSIONS', '*CONTACT EXCLUSIONS', '*CONTACT PAIR', '*CONTACT PROPERTY ASSIGNMENT', '*SURFACE BEHAVIOR', '*FRICTION'):
        report.extend(('ORIGINAL_KEYWORD_BEGIN', block, 'ORIGINAL_KEYWORD_END'))
report.append('ORIGINAL_GENERAL_CONTACT_BLOCKS=%d' % general_count)
report.append('ORIGINAL_ALL_EXTERIOR=%s' % all_exterior)
report.append('ORIGINAL_SELF_PAIR_LINES=%s' % repr(explicit_self_pairs))
report.append('ORIGINAL_EXCLUSION_BLOCKS=%d' % exclusion_count)
report.append('These indicators do not prove full self-contact coverage; surface membership, exclusions and activation steps also matter.')
if general_count == 0:
    property_name = unique_name('CCA_CHECK_HARD', model.interactionProperties)
    contact_property = model.ContactProperty(property_name)
    contact_property.NormalBehavior(pressureOverclosure=HARD, allowSeparation=ON, constraintEnforcementMethod=DEFAULT)
    contact_name = unique_name('CCA_CHECK_GENERAL', model.interactions)
    contact = model.ContactExp(name=contact_name, createStepName='Initial')
    contact.includedPairs.setValuesInStep(stepName='Initial', useAllstar=ON)
    contact.contactPropertyAssignments.appendInStep(stepName='Initial', assignments=((GLOBAL, SELF, property_name),))
    report.append('NEW_DEFAULT_CONTACT=HARD_NORMAL_FRICTIONLESS')
else:
    report.append('EXISTING_GENERAL_CONTACT_PROPERTIES=RETAINED')
model.keywordBlock.synchVersions(storeNodesAndElements=False)
blocks = tuple(model.keywordBlock.sieBlocks)
all_text = '\n'.join(blocks).upper()
surface_name = 'CCA_CHECK_INTERIOR'
counter = 1
while surface_name.upper() in all_text:
    surface_name = 'CCA_CHECK_INTERIOR_' + str(counter)
    counter += 1
surface_lines = ['*Surface, type=ELEMENT, name=' + surface_name]
for set_name in solid_sets:
    surface_lines.append(set_name + ', INTERIOR')
surface_definition = '\n'.join(surface_lines)
inclusions = '\n'.join(('*Contact Inclusions', ',', ', ' + surface_name, surface_name + ','))
output_definition = '\n'.join(('*Output, field, number interval=200', '*Node Output', 'U', '*Element Output', 'S, STATUS', '*Contact Output', 'CSTRESS, CFORCE, CDISP'))
assembly_ends = [index for (index, block) in enumerate(blocks) if keyword_name(block) == '*END ASSEMBLY']
if len(assembly_ends) != 1:
    raise RuntimeError('Expected one assembly in the keyword representation.')
contact_count = 0
for (index, block) in enumerate(blocks):
    key = keyword_name(block)
    if key == '*END ASSEMBLY':
        model.keywordBlock.replace(index, surface_definition + '\n' + block)
    elif key == '*CONTACT':
        model.keywordBlock.replace(index, block + '\n' + inclusions)
        contact_count += 1
    elif key == '*CONTACT INCLUSIONS':
        model.keywordBlock.replace(index, '** Contact domain expanded by CCA contact check')
    elif key == '*CONTACT EXCLUSIONS':
        model.keywordBlock.replace(index, '** User exclusions removed in the checked model')
    elif key == '*END STEP':
        model.keywordBlock.replace(index, output_definition + '\n' + block)
if not contact_count:
    raise RuntimeError('No general contact definition was generated.')
report.extend(('EXTERIOR_SELF_CONTACT=INCLUDED', 'C3D8R_INTERIOR_SELF_CONTACT=INCLUDED', 'INTERIOR_TO_EXTERIOR_CONTACT=INCLUDED', 'USER_CONTACT_EXCLUSIONS=REMOVED_IN_COPY', 'TIES_AND_CONTACT_PAIRS=RETAINED', 'Automatic exclusions associated with ties/contact pairs still apply.', 'Existing general-contact activation steps are retained.', 'Interior surfaces and expanded inclusions are stored as keyword edits.', 'No damage or element-deletion parameters were changed.', 'Penalty contact does not guarantee mathematically zero penetration.', 'After solving, inspect STATUS, COPEN, CPRESS and deformed active elements.', 'Use deformation scale 1 and exclude STATUS=0 elements.', 'Missing contact output is not proof of zero penetration.', 'CDISP availability must match the installed Abaqus release.', 'No analysis was submitted and no result database was opened.'))
job_name = unique_name('CCA_Cutting_ContactCheck', mdb.jobs)
job = mdb.Job(name=job_name, model=model_name, type=ANALYSIS, numCpus=4, numDomains=4, parallelizationMethodExplicit=DOMAIN)
_sw_record_write(job)
input_path = os.path.abspath(job_name + '.inp')
with open(input_path, 'r') as handle:
    written_input = handle.read().upper()
required_tokens = ('*CONTACT INCLUSIONS', surface_name.upper() + ',', ', INTERIOR', 'CSTRESS, CFORCE, CDISP')
for token in required_tokens:
    if token not in written_input:
        raise RuntimeError('Input verification failed: ' + token)
report.append('INPUT_FILE=' + input_path)
report.append('JOB=' + job_name)
report_path = os.path.abspath(job_name + '_audit.txt')
with open(report_path, 'w') as handle:
    handle.write('\n'.join(report) + '\n')
for line in report:
    print(line)
print('AUDIT_FILE=' + report_path)
print('Submit the generated job through Abaqus/CAE Job Manager.')
print('SOUTHWEITION_JOB_PREPARED')

if not _sw_written_jobs:
    _sw_candidates = [n for n in mdb.jobs.keys() if n not in _sw_before_jobs or mdb.jobs[n] is not _sw_before_jobs[n]]
    if len(_sw_candidates) != 1:
        raise RuntimeError('Cannot identify this script job: call writeInput() on exactly the intended job')
    _sw_record_write(mdb.jobs[_sw_candidates[0]], consistencyChecking=OFF)
_sw_name, _sw_inp = _sw_written_jobs[-1]
if not _sw_os.path.isfile(_sw_inp):
    raise RuntimeError('This script did not produce its INP: ' + _sw_inp)
print('SOUTHWEITION_CWD ' + _sw_os.path.dirname(_sw_inp))
print('SOUTHWEITION_JOB ' + _sw_name)
print('SOUTHWEITION_MODEL ' + str(getattr(mdb.jobs[_sw_name], 'model', '')))
print('SOUTHWEITION_INP ' + _sw_inp)
print('SOUTHWEITION_JOB_PREPARED')

