QAT Code: SLIP

From Catcliffe Development
Revision as of 13:50, 25 July 2023 by XenoEngineer (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

'--------------------------------------------------------------------------------
'    Component  : clsSLIP class module
'    Project    : QAT
'    Copyright  : DonEMitchell 2001-2011 Sequim, WA - Cottonwood, AZ
'
'    Prueitt-Mitchell (2011): Completion of the QAT Ontology engine.
'    Version 2.8.X edits opened October 27, 2011
'    Modified   : 10/27/2011-2:29:30 PM on Developer
'--------------------------------------------------------------------------------


Private mcolTopicTree As Collection    ' Holds name-keyed references to clsTopic instances.
Private moTopicSelection As clsTopic    ' Reference object of user-selection.
Private moAtoms As clsAtoms

' Change-events called by this class (invokes event code in the user form when 'raised' in this class)
Public Event TopicCreated(ByRef oTopic As clsTopic)
Public Event TopicSelected(ByRef oTopic As clsTopic)

Public Event PairsImported(ByRef oRoot As clsTopic)
Public Event ReportKeyChanged(ByVal KeyColumn As Long)
Public Event AtomRadialDistributionChanged(ByRef oTopic As clsTopic)
Public Event Atom3DDistributionChanged(ByRef oTopic As clsTopic)
Public Event RemarkAppended(ByVal NewLine As String)
Public Event CommandResponse(ByVal Message As String)
Public Event Progress(ByVal Message As String)


Private mstrPairsArray()        As String

Private mstrAtomsStringBuffer   As String
Private mstrPairsStringBuffer   As String
Private mbooSourceLoaded        As Boolean
Private mbooPairsImported       As Boolean

Private mbooReportGenEnabled    As Boolean
Private mintReportSortColumn    As Integer
Private mlngReportKeyColumn     As Long

Private mintLevelCount()        As Integer      ' Array stores topics-count per level for spacing the TG nodes.
Private mintMickey              As Integer      ' Mouse-button flag.
Private mstrDataPath            As String
Private mstrEventBrowser        As String

' State-flags
Private mbooIterating           As Boolean


Private Sub Class_Initialize()
    On Error Resume Next
    '   Data-structure                  ' Functionality:
    Set mcolTopicTree = New Collection  ' A collection of topic instances by Name, affording tree-hiearchy-navigation through Parent property of clsTopic.
    Set moAtoms = New clsAtoms
    Set moTopicSelection = New clsTopic
End Sub

Private Sub Class_Terminate()
    On Error Resume Next
    Set mcolTopicTree = Nothing
    Set moAtoms = Nothing
    Set moTopicSelection = Nothing
End Sub
 

Public Sub INILoadProperties()
    On Error Resume Next
    
   mstrDataPath = INIGetString("SLIP", "RootTopicName", cDefTopTopicName)
   mbooSaveProperties = INIGetBool("SLIP", "SaveProperties", "1")
End Sub

' Saves the default variables to a private profile (QAT.INI file)
Public Sub INISaveProperties()
    Dim lngResult As Long
    Dim lngCount As Long
    
    ' Save Properties, optionally.
    INIWrite "SLIP", "SaveProperties", CStr(mbooSaveProperties)
    If mbooSaveProperties Then
        INIWrite "SLIP", "RootTopicName", mstrDataPath
    End If
End Sub


Public Function IsTopic(ByVal Name As String) As Boolean
    ' Return True if Name matches at least one name in the Topic collection.
    Dim topicX As clsTopic
    
    ' Loop through the topic-object collection, comparing names until the first match.
    For Each topicX In mcolTopicTree
        If StrComp(topicX.Name, Name, vbTextCompare) = 0 Then 'a case-insensitive match (first)
            IsTopic = True
            Exit For
        End If
    Next
End Function

Public Function topicFindTopic(ByVal Name As String) As clsTopic
    
    '   This assumes all topic names are unique
    
    Dim topicX As clsTopic
    
    On Error Resume Next
    
    For Each topicX In momcolTopicTree
        If StrComp(Name, topicX.Name, vbTextCompare) = 0 Then
            Set topicFindTopic = topicX
            Exit For
        End If
    Next
End Function

Public Function topicMembersFile(ByVal oTopic As clsTopic) As String

End Function

Public Function topicSortedReport(ByRef oTopic As clsTopic) As String
    '   Load the Report.txt contents.
    '   Split into an array.
    '   Sort by ReportSortColumn.
    '   Re-concatenate a CR delimited report string and return it.
    
    Dim ndx As Long
    Dim ndxDeepest As Long
    Dim strFilespec As String
    Dim strReport As String
    Dim strReportHeader As String
    Dim strReportArray() As String 'array
    Dim strFirst As String
    Dim strSecond As String
    Dim strTemp As String
        
    On Error GoTo Trap
    
    '   Load the Report.txt file
    strFilespec = Root.WhsePath & oTopic.Path & "\Report.txt"
    
    With New clsFile
        If .FileExists(strFilespec) Then
            
            '   The report now exists by the same filespec, read it into variables
            strReportHeader = .ReadLine
            strReport = Mid$(.ReadAll, Len(strReportHeader) + 3)    '   read everything past the header line
            
            '   Remove trailing outer delimiters
            TrimTrailingCharacters strReport, vbCrLf
            
            '   Split into an array at the outter delimiting CRs
            strReportArray = Split(strReport, vbCrLf)
        
            '   Sort the Array on ReportSortColumn
            DataSortStringArray strReportArray
            
            '   Now concatenate an output string from the sorted array
            '   Start with a header
                topicSortedReport = strReportHeader & vbCrLf
            
            '   concatenate each array element with CR delimitation
            For ndx = 0 To UBound(strReportArray)
                topicSortedReport = topicSortedReport & strReportArray(ndx) & vbCrLf
            Next ndx
            
            '   Remove trailing outer delimiter from concatenation
            topicSortedReport = modMain.TrimTrailingCharacters(topicSortedReport, vbCrLf)
                                
            .CloseFileRead
            
            '   Save to file
            If Not .Save(topicSortedReport, strFilespec) Then
                RaiseEvent CommandResponse("Unable to save the sorted report to file " & strFilespec)
                MsgBox "Unable to save the sorted report to file " & strFilespec, vbExclamation
            End If
                         
        End If
    End With
    
ExitPoint:
Exit Function
Trap:
    topicSortedReport = "Error encountered: Unable to sort " & oTopic.Name & " on column " & DataReportSortColumn & " of columns 0-7"
    RaiseEvent CommandResponse("Unable to sort " & oTopic.Name & " on column " & DataReportSortColumn & " of columns 0-7")
    Resume ExitPoint
    Resume
End Function

Public Function topicTopic(ByVal Path As String) As clsTopic
    On Error Resume Next
    
    Set topicTopic = momcolTopicTree.Item(Path)
End Function

Public Function topicTopicRemark(ByVal oTopic As clsTopic) As String

End Function

Public Function ReportWhseRecords(ByRef Atoms As clsAtoms) As String
    '   Extract all warehouse records that match to any
    '   atom name in the MatchColumn specified by DataReportKeyColumn
    '   Return the report-string.
    
    Dim strWhse As String
    Dim strRecords() As String 'array
    Dim strFields() As String 'array
    Dim atomX As clsAtom
    Dim ndx, ndxProgress As Long
    Dim strReportFilespec As String
    Dim strReport As String
    
    
    On Error Resume Next
    
    RaiseEvent CommandResponse("Generating a warehouse data report...")
    
    '   Enable the report loop
    mbooReportGenEnabled = True
    
    strReportFilespec = mstrDataPath & "\" & "WhseReport.txt"
    
    With New clsFile
        
        If .FileExists(Root.WhsePath & "\Datawh.txt") Then 'redundant existence test, but validates folder edits
            '   Read the warehouse data file contents to a working string
            strWhse = .ReadAll
            
            '   Remove all trailing outer-delimiters from the whse
            modMain.TrimTrailingCharacters strWhse, vbCrLf
                        
            '   Split the warehouse data string into an array at the CRs
            strRecords = Split(strWhse, vbCrLf)
                
            '   Start the report with the header line from the whse
            strReport = strRecords(0) & vbCrLf
                
            '   Convention requires the warehouse data file
            '   contains column names as line one, therefore
            '   array element zero contains column names.
            
            '   Loop through all atoms, using the atom
            '   name as the look-up key for matches
            '   of the atom name with the specified key column
                For Each atomX In Atoms
                    '   Loop through all records, and
                    '   examine the MatchColumn specified for a match with the Atom name
                    '   Concatenate the matching records as a
                    '   sub-set of the Warehouse, and return the sub-set
                    If mbooReportGenEnabled Then
                        ndxProgress = ndxProgress + 1
                        RaiseEvent Progress(CStr(ndxProgress) & ":" & CStr(ndx))
                        
                        '   Yield to system needs (systemic affordance)
                        '   allowing report cancellation with Stop command
                        DoEvents
                        
                        For ndx = 1 To UBound(strRecords) 'skip element zero, the whse column header
                            
                            If ndx Mod 5000 = 0 Then RaiseEvent Progress(CStr(ndxProgress) & ":" & CStr(ndx))
                            DoEvents
                            
                            If Not mbooReportGenEnabled Then Exit Function
                            
                            '   Split the record into an array at the Tabs
                            strFields = Split(strRecords(ndx), vbTab)
                            
                            If Len(strRecords(ndx)) Then
                                '   Check for a match with key on the specified MatchColumn
                                '   and concatenate the return as a CR delimited string
                                If StrComp(strFields(DataReportKeyColumn), atomX.Name) = 0 Then
                                    strReport = strReport & strRecords(ndx) & vbCrLf
                                End If
                            End If
                            
                        Next ndx

                        '   Save the report while in progress of looping through atoms
                        With New clsFile
                            If .Save(strReport, strReportFilespec) Then
                                '   Reselect
                                Set Context.TopicSelection = Context.TopicSelection
                                RaiseEvent CommandResponse("Unable to save the report on member " & atomX.Name & " to file " & strReport)
                            End If
                        End With
                    
                    Else
                        
                        MsgBox "Report aborted", vbInformation
                        RaiseEvent CommandResponse("Report generation aborted")
                        Exit For
                    End If ' mbooReportGenEnabled
                Next atomX
                                
                '   Notify interested agencies
                RaiseEvent CommandResponse("Report generated from warehouse records for matches in column " & CStr(DataReportKeyColumn) & " (zero-base)")
                
                '   Remove the trailing CR
                strReport = modMain.TrimTrailingCharacters(strReport, vbCrLf)
        Else
            RaiseEvent CommandResponse("Data warehouse not available: " & Root.WhsePath & "\Datawh.txt")
            MsgBox "Data warehouse not available: " & Root.WhsePath & "\Datawh.txt"
        End If
        
        
        '   Return the report
        ReportWhseRecords = strReport
                
    End With
End Function
'
'Public Function CreateAttribute(ByVal Name As String, ByVal Value As String) As clsAttribute
'    On Error Resume Next
'
'    Set CreateAttribute = New clsAttribute
'    With CreateAttribute
'        .Name = Name
'        .Value = Value
'    End With
'End Function


'   Instance constructors

Public Function CreateTopTopic( _
            ByVal Name As String, _
            ByVal x As Long, _
            ByVal Y As Long) As clsTopic
    Dim oNew As clsTopic
    
    On Error Resume Next
    
    Set oNew = New clsTopic 'create a concrete type for return
    
    With oNew
        .Name = Name
        .MetricsX = x
        .MetricsY = Y
        .RadialMagnification = 30 'Default
        Set .Atoms = DataLoadMembers(Root.WhsePath & "\" & Name & "\Members.txt")
    End With
    
    
    '   Add a reference in the TopicSpace
    mcolTopicTree.Add oNew, oNew.Path  'keyed by path
    
    '   Notify the world
    RaiseEvent TopicCreated(oNew)
    
    '   Return the new member
    Set CreateTopTopic = oNew

    Set oNew = Nothing
End Function

Public Function CreateTopic( _
            ByVal Name As String, _
            ByVal x As Long, _
            ByVal Y As Long, _
            oParent As clsTopic) As clsTopic
    '   Construct a concrete clsTopic instance.
    
    Dim oNewTopic As clsTopic
    
    On Error Resume Next
    
    Set oNewTopic = New clsTopic
    With oNewTopic
        .Name = Name
        Set .TopicParent = Me
        
        Set .Atoms = DataLoadMembers( _
            MembersFileSpec:=Root.WhsePath & oParent.Path & "\" & Name & "\Members.txt")
        .MetricsX = x
        .MetricsY = Y
        .RadialMagnification = 30 'larger default for non-root levels
    End With
    
    '   Add a reference to the new topic in the Children
    '   collection of the Parent Topic, keyed by Name
    '   This creates parent-child linking
    oParent.TopicChildren.Add oNewTopic, oNewTopic.Name
    
    '   Add a reference in the TopicSpace keyed by path
    mcolTopicTree.Add oNewTopic, oNewTopic.Path 'keyed by path
    
    '   Notify the world
    RaiseEvent TopicCreated(oNewTopic)
    
    Set CreateTopic = oNewTopic
    Set oNewTopic = Nothing
End Function


Public Function DataAppendRemarkFile( _
            ByVal NewLine As String, _
            ByVal Filespec As String, _
            Optional SendEventMsg As Boolean = True) As Boolean
    
    '   Save the file and send a RemarkAppended message, maybe
    On Error GoTo Trap
    
    With New clsFile
        .OpenForAppend Filespec
        .WriteLine NewLine
        
        If SendEventMsg Then RaiseEvent RemarkAppended(NewLine)
        RaiseEvent CommandResponse("Remark line appended")
    End With
    
    DataAppendRemarkFile = True

ExitPoint:
    Exit Function

Trap:
    'Append contextual information into existing err object Description
    Err.Description = "SLIP.clsSLIP.DataAppendRemarkFile: " & Err.Description & " - File: " & Filespec
    Err.Raise Err.Number
    
    Resume ExitPoint
End Function

Public Property Get DataAtoms() As clsAtoms
    Set DataAtoms = moAtoms
End Property

Public Function DataReadReportKey() As Boolean
    Dim strConjecture As String
    
    On Error GoTo Trap
    
    With New clsFile
    
        '   Read the conjecture file for the report key column (B)
        If .FileExists(mstrDataPath & "\Conjecture.txt") Then
            strConjecture = .ReadAll()
            DataReportKeyColumn = Split(strConjecture, vbTab)(0)
            RaiseEvent CommandResponse("Report key Column set to " & CStr(DataReportKeyColumn))
            DataReadReportKey = True
        Else
            DataReportKeyColumn = 4
            RaiseEvent CommandResponse("Conjecture data not found -defaulting report key column to 4")
        End If
    End With
    
    RaiseEvent CommandResponse("Reading ReportKeyColumn from Conjecture data")
        
    Exit Function
Trap:
    RaiseEvent CommandResponse("Report Key Column not set. Error reading " & mstrDataPath & "\Conjecture.txt" & Err.Description)
        
End Function

Public Property Get DataPairsArray() As String()
    DataPairsArray = mstrPairsArray
End Property

Public Property Let DataReportGenerationEnabled(ByVal RHS As Boolean)
    mbooReportGenEnabled = RHS
End Property

Public Property Get DataReportGenerationEnabled() As Boolean
    DataReportGenerationEnabled = mbooReportGenEnabled
End Property

Public Property Get DataReportKeyColumn() As Long
    DataReportKeyColumn = mlngReportKeyColumn
End Property

Public Property Let DataReportKeyColumn(ByVal Value As Long)
    mlngReportKeyColumn = Value
    RaiseEvent ReportKeyChanged(Value)
End Property

Public Property Let DataReportSortColumn(ByVal RHS As Integer)
    mintReportSortColumn = RHS
End Property

Public Property Get DataReportSortColumn() As Integer
    DataReportSortColumn = mintReportSortColumn
End Property

Public Sub DataRotateAtoms3DUnused(oTopic As clsTopic, ByVal Rotation As Integer, ByVal Incline As Integer)
    '   Convert each (x,y,z) point to a vector (angleI, angleR, length)
    '   Increment the cyphered vector by the passed parameters
    '   Convert the incremented vector back to a Cartesian coordinates
    '   and update the atom with the new (x,y,z) coordinates
    
    Dim atomX As clsAtom
    Dim x, Y, Z As Single
    
    For Each atomX In oTopic.Atoms
        
    Next
    
End Sub

Public Sub DataSortStringArray(ByRef SourceArray() As String)
    Dim ndx, ndxDeepest As Long
    Dim strFirst As String
    Dim strSecond As String
    Dim strTemp As String
    
    ndx = 0
    
    On Error Resume Next
    
    '   Sort the Array on KeyColumn into ordinal rank predicated by string compare method
    Do While ndx < UBound(SourceArray) - 1
    
        '   Retreive the keycolumn values of two successive array elements which are split by tabs
        strFirst = Split(SourceArray(ndx), vbTab)(mintReportSortColumn) 'default to zero, temporal sort
        strSecond = Split(SourceArray(ndx + 1), vbTab)(mintReportSortColumn)
                
        Select Case StrComp(strFirst, strSecond)
            Case -1, 0 ' ndx1 <= ndx2
                ndx = ndxDeepest + 1
                ndxDeepest = ndx
            Case 1 'ndx1 > ndx2
                '   Swap the inordinate pair
                strTemp = SourceArray(ndx + 1)
                SourceArray(ndx + 1) = SourceArray(ndx)
                SourceArray(ndx) = strTemp

                '   Store the deepest progress
                If ndx > ndxDeepest Then ndxDeepest = ndx
                
                '   Backup the index by one
                '   to reinspect the prior pair,
                '   unless its the top element 1
                If ndx > 0 Then ndx = ndx - 1

        End Select
        'Debug.Print CStr(ndx) & String(ndx + 1, "-") & "+"
    Loop
End Sub

'Public Property Get DataWarehouseArray() As String()
'    DataWarehouseArray = mstrPairsArray
'End Property
'
Public Property Get DataAtomsStringBuffer() As String
    DataAtomsStringBuffer = mstrAtomsStringBuffer
End Property

Public Function DataClearTopicGraph() As Boolean
    ' Clear everything but
    '    the Functionality Singleton instance
    '    the GUID, as this instance still exists
    '    and the TopicMetrics fields, of the top Topic,
    '    being situated context information.

    Set mcolTopicTree = New Collection
    
    Set ContextTopicSelection = Nothing
    mbooIterating = False
    ReDim mintLevelCount(0)
    mstrAtomsStringBuffer = ""
    mstrPairsStringBuffer = ""
    mbooSourceLoaded = False
    mbooPairsImported = False
    ReDim mstrPairsArray(0)
    mbooIterating = False
    mbooReportGenEnabled = True
    mintReportSortColumn = cDefWhseRepSortCol
    mlngReportKeyColumn = cDefWhseRepKeyCol
    
    ReDim mintLevelCount(0)
    mintMickey = 0
    mstrDataPath = ""
    mstrEventBrowser = ""

    '   Send a message.
    RaiseEvent CommandResponse("Topic Graph Cleared")
    
End Function

Public Sub DataClusterAtomsRadial(ByRef oTopic As clsTopic, Optional ByVal MaxKiloIters As String = "100")

    Dim AtomA As clsAtom
    Dim AtomB As clsAtom
    Dim intGap As Integer
    Dim intDifference As Integer
    Dim ndx As Long 'max value past four billion
    Dim strPair As String 'holds "**" delimited pair-names of two randomly selected Atoms
    Dim atomX As clsAtom
    Dim strMember As String
    Dim intAngle1 As Integer
    Dim intAngle2 As Integer
    Dim strTemp As String
    Dim strTabulation As String
    
    Dim lngMaxIters As Long
    Dim lngStartingIters As Long
    
    Dim col As Collection 'pair counter with hash
    
    
    'Const cHeat = 0.05
    'Const cHeat = 0.3
    Const cHeat = 0.67
    'Const cHeat = 1
    'Const cHeat = 1.33
    
    On Error Resume Next
    
    Set col = New Collection
    
    Screen.MousePointer = vbArrowHourglass  '   because DoEvents is in a loop,
                                            '   allowing IterationEnabled to be toggled
                                            '   while iterating
    '   Cypher stop point
    lngMaxIters = CSng(MaxKiloIters) * 1000
    lngStartingIters = oTopic.RadialIters
    
    RaiseEvent CommandResponse("Clustering in progress...")
    
    '   Loop until disabled (grammatical Stop)
    Randomize ' Randomize the seed of the Rnd statement
    DataIterationEnabled = True
    
    While mbooIterating And (ndx < lngMaxIters)
        
        DoEvents 'allowing IterationEnabled to be toggled
        
        With oTopic.Atoms
            '   Randomly select two atoms
            Set AtomA = .Item(CInt(Rnd * (.Count - 1)) + 1) 'retrieve Atom by random ordinal
            Set AtomB = .Item(CInt(Rnd * (.Count - 1)) + 1)
        End With
        
        '   Build a name-pair from randomly selected Atoms
        strPair = AtomA.Name & "**" & AtomB.Name
        
        'col.Add strPair, strPair
        
        'If Err = 0 Then
        '    Debug.Print col.Count; strPair
        'End If
        Err.Clear
        
        '   Seek for the presence of the pair in the tabular pairs buffer
        '   If found the pair is moved closer together
        If PairExists(strPair) Then
            'Debug.Print "Pair count: " & CStr(col.Count)
            '   Move the two atoms closer,
            '   leaving the separation at
            '   cHeat portion of the original radial gap
    
            '   Store the smaller degree as Angle1 and larger degree as Angle2
            If (AtomA.Degree < AtomB.Degree) Then
                intAngle1 = AtomA.Degree
                intAngle2 = AtomB.Degree
            Else
                intAngle1 = AtomB.Degree
                intAngle2 = AtomA.Degree
            End If
    
            '   Store the difference in angles
            intGap = intAngle2 - intAngle1
    
            Select Case (intGap)
                Case Is <= 180
                    intAngle1 = (cHeat * intGap / 2) + intAngle1
                    intAngle2 = intAngle2 - (cHeat * intGap / 2)
                Case Is > 180
                    intGap = 360 - intGap
                    intAngle1 = intAngle1 - (cHeat * intGap / 2)
                    intAngle2 = (cHeat * intGap / 2) + intAngle2
            End Select
    
            '   Normalize the RadialAngle, should any have lapsed negative
            If intAngle1 < 0 Then intAngle1 = intAngle1 + 360
            If intAngle1 > 360 Then intAngle1 = intAngle1 - 360
            If intAngle2 < 0 Then intAngle2 = intAngle2 + 360
            If intAngle2 > 360 Then intAngle2 = intAngle2 - 360
    
            AtomA.Degree = intAngle1
            AtomB.Degree = intAngle2
        End If 'PairExists(strPair)
        
        '   Increment the index
        ndx = ndx + 1
    

        '   Every 10000 iterations,
        '   or every attainment of MaxIters,
        '   save and update the plot
        If (ndx Mod 10000) = 0 Or ndx >= lngMaxIters Then
            
            '   Update the memory instance of oTopic
            oTopic.RadialIters = lngStartingIters + ndx
            
            'Debug.Print atomA.Name; atomB.Name
            'Debug.Print oTopic.RadialIters; ndx
            
            '   Send notifacation message
            RaiseEvent AtomRadialDistributionChanged(oTopic)
    
            '   Save the Members tabulation to file
            With New clsFile
                
                '   Convert the Atoms object to a string
                strTabulation = Tabular.SerializeMembers(oTopic.Atoms)
                
                '   Save the string as the current iterated state of members
                .Save _
                        Buffer:=strTabulation, _
                        Filespec:=mstrDataPath & oTopic.Path & "\Members.txt"
                .CloseFileWrite
                
                '   Update the user interface.
                RaiseEvent AtomRadialDistributionChanged(oTopic)
                Stop
            
            End With

        End If
    Wend
    
    oTopic.RadialIters = lngStartingIters + ndx
    DataIterationEnabled = False
    
    RaiseEvent CommandResponse("Clustering iterations completed: " & CStr(ndx))
    
    Screen.MousePointer = vbDefault

End Sub

Public Sub DataClusterAtoms3D(ByRef oTopic As clsTopic, Optional ByVal MaxKiloIters As String = "100")
    Dim AtomA As clsAtom
    Dim AtomB As clsAtom
    Dim intGap As Integer
    Dim intDifference As Integer
    Dim ndx As Long 'max value past four billion
    Dim strPair As String 'holds "**" delimited pair-names of two randomly selected Atoms
    Dim atomX As clsAtom
    Dim strMember As String
    Dim intAngle1 As Integer
    Dim intAngle2 As Integer
    Dim strTemp As String
    Dim strTabulation As String
    
    Dim lngMaxIters As Long
    Dim lngStartingIters As Long
    Dim intMoves As Integer
    
    'Const cHeat = 0.05
    'Const cHeat = 0.3
    Const cHeat = 0.67
    'Const cHeat = 1
    'Const cHeat = 1.33
    
    On Error GoTo Trap
    
    
    '   Cypher stop point
    lngMaxIters = CLng(MaxKiloIters) * 1000
    lngStartingIters = oTopic.RadialIters
    
    RaiseEvent CommandResponse("Clustering in progress...")
       
    Randomize ' Randomize the seed of the Rnd statement
    DataIterationEnabled = True
    
    '   Loop until disabled (grammatical Stop)
    While mbooIterating And (ndx < lngMaxIters)
        
        DoEvents 'allowing IterationEnabled to be toggled
        
        With oTopic.Atoms
            '   Randomly select two atoms
            Set AtomA = .Item(CInt(Rnd * (.Count - 1)) + 1) 'retrieve Atom by random ordinal
            Set AtomB = .Item(CInt(Rnd * (.Count - 1)) + 1)
        End With
        
        '   Build a name-pair from randomly selected Atoms
        strPair = AtomA.Name & "**" & AtomB.Name
        
        '   Seek for the presence of the pair in the tabular pairs buffer
        '   If found the pair is moved closer together
        
        If PairExists(strPair) Then
            '   Reduce the 3D-space distance between the pair

            'newDistance = Metrics.AtomicDistance(AtomA, AtomB) / cHeat
            With AtomA
                .MetricsX = .MetricsX + cHeat * (AtomB.MetricsX - .MetricsX) / 2
                .MetricsY = .MetricsY + cHeat * (AtomB.MetricsY - .MetricsY) / 2
                .MetricsZ = .MetricsZ + cHeat * (AtomB.MetricsZ - .MetricsZ) / 2
            End With
            With AtomB
                .MetricsX = .MetricsX - cHeat * (.MetricsX - AtomA.MetricsX) / 2
                .MetricsY = .MetricsY - cHeat * (.MetricsY - AtomA.MetricsY) / 2
                .MetricsZ = .MetricsZ - cHeat * (.MetricsZ - AtomA.MetricsZ) / 2
            End With
        End If 'PairExists(strPair)
        '   Increment the index
        ndx = ndx + 1
    
        '   Every 10000 iterations,
        '   or every attainment of MaxIters,
        '   save and update the plot
        If (ndx Mod 10000) = 0 Or ndx >= lngMaxIters Then
            
            '   Update the memory instance of oTopic
            oTopic.RadialIters = lngStartingIters + ndx
            
            'Debug.Print atomA.Name; atomB.Name
            'Debug.Print oTopic.RadialIters; ndx
            
            '   Send notifacation message
            RaiseEvent Atom3DDistributionChanged(oTopic)
   'GoTo fu
            '   Save the Members tabulation to file
            With New clsFile
            
                '   Convert the Atoms object to a string tabulation
                strTabulation = Tabular.SerializeMembers(oTopic.Atoms)
                
                '   Save the Members tabulation to file
                .Save _
                        Buffer:=strTabulation, _
                        Filespec:=mstrDataPath & oTopic.Path & "\Members.txt"
                .CloseFileWrite
                
                '   Send notifacation message
                RaiseEvent Atom3DDistributionChanged(oTopic)
            
            End With
fu:
        End If
    Wend
    
    oTopic.RadialIters = lngStartingIters + ndx
    
    RaiseEvent CommandResponse(CStr(ndx) & " 3D Clustering iterations completed")
    
ExitPoint:
    
Exit Sub
Trap:
    
    Resume ExitPoint
    Resume
End Sub


Function PairExists(ByVal Pair As String) As Boolean
    '   Pair is compared against the middle, then successive
    '   binary division of remaining middles on either
    '   left or right of the compare point,
    '   until the exact match is found, which returns true,
    '   or until there are no further records to divide, returning false
    
    '   The mstrPairsArray is assumed to be sorted in ascending rank
    '   The ordination of rank among the array names then become a
    '   self-index into the array by value of the array element, based on a successive binary division
    '   of left or right halves, pending a string comparison returning
    '   a three-state result: less, equal or greater.
    
    Dim lngSmaller As Long
    Dim lngMiddle As Long
    Dim lngLarger As Long
    Dim ndxAttempts As Integer
    
    On Error Resume Next
    
    lngSmaller = 0
    lngLarger = UBound(mstrPairsArray)
    
    'Debug.Print "Total Pairs: " & UBound(mstrPairsArray) + 1
    
    Do While lngSmaller < lngLarger
        
        '   Place a middle ndx in the current bounded block
        lngMiddle = lngSmaller + ((lngLarger - lngSmaller) / 2)
        
        '   Based on the case of StrComp,
        '   Move either the smaller or larger index to the middle
        '   Until middle locates on a match
        Select Case StrComp(Pair, mstrPairsArray(lngMiddle))
            
            Case -1 'Pair < Array middle
                '   Adjust the block boundaries
                lngLarger = lngMiddle - 1
                ndxAttempts = ndxAttempts + 1
                'Debug.Print ndxAttempts;
            
            Case 0 ' a match
                lngSmaller = lngLarger 'satisfies loop
                PairExists = True
            
            Case 1 'Pair > Array middle
                '   Adjust the block boundaries
                lngSmaller = lngMiddle + 1
                ndxAttempts = ndxAttempts + 1
                'Debug.Print ndxAttempts;
        
        End Select
    Loop
    'Debug.Print String(ndxAttempts, "-") & "+"
Trap:
End Function

Public Function DataExtractAtoms(Optional ByVal TopicName As String = "A1") As Boolean
    Dim ndx As Long
    Dim lngCount As Long
    Dim strSplitPairs() As String 'array
    
    Dim lngLength As Long
    Dim strAtom As String
    Dim strPairs As String
    Dim intExtractions As Integer
      
    On Error Resume Next
    
    mintMickey = Screen.MousePointer
    Screen.MousePointer = vbHourglass
    
    RaiseEvent CommandResponse("Extracting unique members from Paired.txt...")
    DoEvents
    
    '   Clear Atoms buffer and copy the Pairs to
    '   strPairs for conditioning
    mstrAtomsStringBuffer = ""
    strPairs = mstrPairsStringBuffer
    
    '   Flatten the inner and outer delimiters to the same (Replace "**" with CRLF )
    strPairs = Replace(strPairs, "**", vbCrLf)

    '   Split the delimited members to an array
    strSplitPairs = Split(strPairs, vbCrLf)
    
    '   Loop through array, checking for new Members from Pairs
    lngCount = UBound(strSplitPairs)
    For ndx = 0 To lngCount
        '   Test that the array element is in the Atoms buffer
        If InStr(1, mstrAtomsStringBuffer, strSplitPairs(ndx)) = 0 Then 'not there
            '   Concatenate the new randomized atom to the buffer at the beginning
            mstrAtomsStringBuffer = strSplitPairs(ndx) & vbTab & CStr(Int(Rnd * 360) + 1) & vbCrLf & mstrAtomsStringBuffer
            intExtractions = intExtractions + 1
        End If
        If ndx Mod 100 = 0 Then
            RaiseEvent Progress("Extracting atoms... " & CStr(ndx))
            DoEvents
        End If
    Next
      
    '   Remove the trailing delimiter
    mstrAtomsStringBuffer = Left$(mstrAtomsStringBuffer, Len(mstrAtomsStringBuffer) - 1)
    
    '   The reduced set of Pairs is now a unique set of delimited Atoms in mstrAtomsStringBuffer.
    
    '   Write the fresh Members data to file as the Members of the top Topic named
    '   the value in 'TopicName' in a folder named by the value in 'TopicName'
    With New clsFile
        
        '   Create a new top distribution if non-existant,
        '   or query the user for overwrite permission,
        '   else bail, no persistence -- no existence
        Select Case .FileExists(Root.WhsePath & "\" & TopicName & "\Members.txt")
            
            Case True
                
                If MsgBox("Overwrite the Members.txt file for Topic " & TopicName & "?", vbYesNo + vbQuestion + vbDefaultButton2, "Random distribution available") = vbYes Then
                    If .Save(mstrAtomsStringBuffer, UseAppend:=False) Then
                        '   Create (or err past if existing) a folder for the top node, named A1
                        DataExtractAtoms = True
                    Else
                        RaiseEvent CommandResponse("Unable to save the Atoms extracted from Paired.txt to " & .Filespec)
                        MsgBox "Unable to save the Atoms extracted from Paired.txt to: " & .Filespec
                    End If
                End If
            
            Case False
                
                '   Create the new Topic Folder
                MkDir Root.WhsePath & "\" & TopicName
                
                                
                If .Save(mstrAtomsStringBuffer) Then
                    RaiseEvent CommandResponse("Extracted Atoms saved to " & .Filespec)
                    DataExtractAtoms = True
                Else
                    RaiseEvent CommandResponse("Unable to save the Atoms extracted from Paired.txt to: " & .Filespec)
                    MsgBox "Unable to save the Atoms extracted from Paired.txt to: " & .Filespec
                End If
        
        End Select
    End With
    
    RaiseEvent CommandResponse("Atoms extracted: " & CStr(intExtractions))
    
    Screen.MousePointer = mintMickey
    
End Function

Public Function DataExtractSelectedAtoms(ByVal TopicName As String) As Boolean
    '   On the selected topic,
    '   Tabulate a string concatenation of Atom names and RadialAngles
    '   between starting and ending RadialSelection degrees
    '   and save them into a a new folder created,
    '   named by the value of TopicName
    ' Format: AtomCategoryValue vbTab Degree
    '         where Degree is 1 to 360.
    
    Dim intCount As Integer
    Dim strRadialSelection As String
    Dim strSplitPairs() As String 'array
    
    Dim lngLength As Long
    Dim atomX As clsAtom
      
    On Error Resume Next
    
    mintMickey = Screen.MousePointer
    Screen.MousePointer = vbHourglass

    RaiseEvent CommandResponse("Extracting selected member atoms from " & Context.TopicSelection.Name)
    
    '   Loop through all atoms of the context selected
    '   and string concatenate a tabular file stream
    With Context.TopicSelection
        For Each atomX In .Atoms
            If atomX.Degree >= RadialSelectionStart Then
                If atomX.Degree <= .Meta.SelectEnd Then
                    strRadialSelection = strRadialSelection & atomX.Name & vbTab & CStr(atomX.Degree) & vbCrLf
                    intCount = intCount + 1
                End If
            
            End If
        Next
    End With 'Context.TopicSelection
      
    '   Remove the trailing delimiter
    strRadialSelection = Left$(strRadialSelection, Len(strRadialSelection) - Len(vbCrLf))
    
    '   Write the fresh selection Members data to file as the Members of the new Topic named
    '   the value in 'TopicName' in a folder named by the value in 'TopicName'
    With New clsFile
        
        '   Create a new Members.txt file if non-existant,
        '   or query the user for overwrite permission,
        '   else bail, no persistence -- no existence
        Select Case .FileExists(Root.WhsePath & Context.TopicSelection.Path & "\" & TopicName & "\Members.txt")
            
            Case True
                
                If MsgBox("Overwrite the Members.txt file for Topic " & TopicName & "?", vbYesNo + vbQuestion + vbDefaultButton2, "Random distribution available") = vbYes Then
                    If .Save(strRadialSelection, UseAppend:=False) Then
                        '   Create (or err past if existing) a folder for the top node, named A1
                        DataExtractSelectedAtoms = True
                    Else
                        MsgBox "Unable to save the Atoms Selected to: " & .Filespec
                    End If
                End If
            
            Case False
                
                '   Create the new Topic Folder
                MkDir Root.WhsePath & Context.TopicSelection.Path & "\" & TopicName   'errors if exists
                
                                
                If Not .Save(strRadialSelection) Then 'any sort of error will cause the next line to execute by default of resume-next
                    RaiseEvent CommandResponse("Unable to save the extractted members to: " & .Filespec)
                    MsgBox "Unable to save the Atoms extracted from Paired.txt to: " & .Filespec
                Else
                    DataExtractSelectedAtoms = True
                End If
        
        End Select
    End With
    
    RaiseEvent CommandResponse("Atom 'Extraction' successful for selected radial-members as " & TopicName & ". " & CStr(intCount) & " radial-members extracted.")
    
    Screen.MousePointer = mintMickey
    
End Function

Public Function DataExtractResidualAtomsBuggy(Optional ByVal ResidualTopicName As String = "R") As Boolean
    '   On the selected topic,
    '   Tabulate a string concatenation of Atom names and RadialAngle
    '   that are not found in a child,
    '   and save them into a new folder created,
    '   named by the value of ResidualTopicName
    
    Dim ndx, ndxDeepest As Long
    Dim strResidual As String
    Dim strAtoms() As String 'array
    
    Dim lngLength As Long
    Dim topicX As clsTopic
    Dim atomX As clsAtom
    Dim strTemp As String
    
    On Error Resume Next
    
    '   Dimension a storage array to hold all of the atoms of all of the child topics
    '   This number will be no bigger than the atom count of the topic.
    ReDim strAtoms(Context.TopicSelection.Atoms.Count)
    
    '   Loop through all atoms of all children of the selected context
    '   and string concatenate a tabular file stream
    With Context.TopicSelection
        For Each topicX In .Structure.Children
            For Each atomX In topicX.Atoms
                strAtoms(ndx) = atomX.Name
                ndx = ndx + 1
                'Debug.Print topicX.Name; ndx
            Next
        Next
        '   Redim the atoms array to loose any trailing unused array elements
        ReDim Preserve strAtoms(ndx - 1)
        
        ndx = 0
        '   Sort the Array into ordinal rank predicated by string compare method
        Do While ndx < UBound(strAtoms) - 1
            Select Case StrComp(strAtoms(ndx), strAtoms(ndx + 1))
                Case -1, 0 ' ndx1 <= ndx2
                    ndx = ndxDeepest + 1
                    ndxDeepest = ndx
                Case 1 'ndx1 > ndx2
                    '   Swap the inordinate pair
                    strTemp = strAtoms(ndx + 1)
                    strAtoms(ndx + 1) = strAtoms(ndx)
                    strAtoms(ndx) = strTemp

                    '   Store the deepest progress
                    If ndx > ndxDeepest Then ndxDeepest = ndx
                    
                    '   Backup the index by one
                    '   to reinspect the prior pair,
                    '   unless its the top pair
                    If ndx > 0 Then ndx = ndx - 1

            End Select
            'Debug.Print CStr(ndx) & String(ndx + 1, "-") & "+"
        Loop
        Dim ndx2 As Integer
        ndx = 0
        For Each atomX In .Atoms
            '   Concatenate a file stream tabulation
            '   of all atoms that are not in the strAtoms array
            If Not ElementExists(strAtoms, atomX.Name) Then
                ndx = ndx + 1
                strResidual = strResidual & atomX.Name & vbTab & CStr(atomX.Degree) & vbCrLf
            Else
                ndx2 = ndx2 + 1
            End If
        Next
        'Debug.Print ndx; ndx2
      
        '   Remove the trailing delimiter
        strResidual = Left$(strResidual, Len(strResidual) - Len(vbCrLf))
    
    End With 'Context.TopicSelection
    
      
      
    '   Write the fresh selection Members data to file as the Members of the new Topic named
    '   the value in 'ResidualTopicName' in a folder named by the value in 'ResidualTopicName'
    With New clsFile
        
        '   Create a new Members.txt file if non-existant,
        '   or query the user for overwrite permission,
        '   else bail, no persistence -- no existence
        Select Case .FileExists(Root.WhsePath & Context.TopicSelection.Path & "\" & ResidualTopicName & "\Members.txt")
            
            Case True
                
                If MsgBox("Overwrite the Members.txt file for Topic " & ResidualTopicName & "?", vbYesNo + vbQuestion + vbDefaultButton2, "Random distribution available") = vbYes Then
                    If .Save(strResidual, UseAppend:=False) Then
                        '   Create (or err past if existing) a folder for the top node, named A1
                        DataExtractResidualAtomsBuggy = True
                    Else
                        MsgBox "Unable to save the Atoms Selected to: " & .Filespec
                    End If
                End If
            
            Case False
                
                '   Create the new Topic Folder
                MkDir Root.WhsePath & Context.TopicSelection.Path & "\" & ResidualTopicName   'errors if exists
                
                                
                If Not .Save(strResidual) Then 'any sort of error will cause the next line to execute by default of resume-next
                    MsgBox "Unable to save the Atoms extracted from Paired.txt to: " & .Filespec
                Else
                    DataExtractResidualAtomsBuggy = True
                End If
        
        End Select
    End With
    
    Set atomX = Nothing
    Set topicX = Nothing
End Function

Public Function DataExtractResidualAtoms(Optional ByVal ResidualTopicName As String = "R") As Boolean
    '   On the selected topic,
    '   Tabulate a string concatenation of Atom names and RadialAngle
    '   that are not found in a child,
    '   and save them into a new folder created,
    '   named by the value of ResidualTopicName
    
    Dim ndx, ndx2, ndxDeepest As Long
    Dim strResidual As String
    Dim strSiblingAtoms As String
    
    Dim lngLength As Long
    Dim topicX As clsTopic
    Dim atomX As clsAtom
    Dim strTemp As String
    
    On Error Resume Next
    
    mintMickey = Screen.MousePointer
    Screen.MousePointer = vbHourglass
    
    RaiseEvent CommandResponse("Extracting residual member atoms from " & Context.TopicSelection.Name)
    
    '   Loop through all atoms of all children of the selected context
    '   and string concatenate a sibling atom delimitation
    With Context.TopicSelection
        For Each topicX In .Structure.Children
            For Each atomX In topicX.Atoms
                strSiblingAtoms = strSiblingAtoms & "|" & atomX.Name
                ndx = ndx + 1
            Next
        Next
        
        'Debug.Print "Total Atoms: " & CStr(ndx)
        
        For Each atomX In .Atoms
            '   Concatenate a file stream tabulation
            '   of all atoms that are not in the strSiblingAtoms array
            'Debug.Print InStr(1, strSiblingAtoms, atomX.Name)
            If InStr(1, strSiblingAtoms, atomX.Name) = 0 Then 'residual found
                ndx = ndx + 1
                strResidual = strResidual & atomX.Name & vbTab & CStr(atomX.Degree) & vbCrLf
            Else
                ndx2 = ndx2 + 1
            End If
        Next
        
        'Debug.Print ndx; ndx2
      
        '   Remove the trailing delimiter
        strResidual = Left$(strResidual, Len(strResidual) - Len(vbCrLf))
    
    End With 'Context.TopicSelection
    
      
      
    '   Write the fresh selection Members data to file as the Members of the new Topic named
    '   the value in 'ResidualTopicName' in a folder named by the value in 'ResidualTopicName'
    With New clsFile
        
        '   Create a new Members.txt file if non-existant,
        '   or query the user for overwrite permission,
        '   else bail, no persistence -- no existence
        Select Case .FileExists(Root.WhsePath & Context.TopicSelection.Path & "\" & ResidualTopicName & "\Members.txt")
            
            Case True
                
                If MsgBox("Overwrite the Members.txt file for Topic " & ResidualTopicName & "?", vbYesNo + vbQuestion + vbDefaultButton2, "Random distribution available") = vbYes Then
                    If .Save(strResidual, UseAppend:=False) Then
                        '   Create (or err past if existing) a folder for the top node, named A1
                        DataExtractResidualAtoms = True
                    Else
                        RaiseEvent CommandResponse("Unable to save the residual members to: " & .Filespec)
                        MsgBox "Unable to save the residual members to: " & .Filespec
                    End If
                End If
            
            Case False
                
                '   Create the new Topic Folder
                MkDir Root.WhsePath & Context.TopicSelection.Path & "\" & ResidualTopicName   'errors if exists
                
                                
                If Not .Save(strResidual) Then 'any sort of error will cause the next line to execute by default of resume-next
                    RaiseEvent CommandResponse("Unable to save the residual members to: " & .Filespec)
                    MsgBox "Unable to save the residual members to: " & .Filespec
                Else
                    DataExtractResidualAtoms = True
                End If
        
        End Select
    End With
    
    RaiseEvent CommandResponse(CStr(ndx) & " residual members of topic " & Context.TopicSelection.Name & " extracted to topic " & ResidualTopicName)
    Screen.MousePointer = mintMickey
    
    Set atomX = Nothing
    Set topicX = Nothing
End Function


Function ElementExists(ByRef SourceArray As Variant, ByVal Element As String) As Boolean
    '   Element is compared against element in the middle,
    '   then successive binary division of remaining middles
    '   on either left or right of the compare point,
    '   until the exact match is found, which returns true,
    '   or until there are no further element locations to divide, returning false
    
    '   The SourceArray is assumed to be sorted in ascending or descending rank
    '   The ordination of rank among the array names then become a
    '   self-index into the array, based on a successive binary division
    '   of left or right halves, pending a string comparison returning
    '   a three-state result, less, equal, greater.
    Dim lngSmaller As Long
    Dim lngMiddle As Long
    Dim lngLarger As Long
    Dim ndxAttempts As Integer
    
    On Error Resume Next
    
    lngSmaller = 0
    lngLarger = UBound(SourceArray)
    
    'Debug.Print "Total Pairs: " & UBound(SourceArray) + 1
    
    Do While lngSmaller < lngLarger
        
        '   Place a middle ndx in the current bounded block
        lngMiddle = lngSmaller + ((lngLarger - lngSmaller) / 2)
        
        '   Based on the case of StrComp,
        '   Move either the smaller or larger index to the middle
        '   Until middle locates on a match
        Select Case StrComp(Element, SourceArray(lngMiddle))
            
            Case -1 'Element < Array middle
                '   Adjust the block boundaries
                lngLarger = lngMiddle - 1
                ndxAttempts = ndxAttempts + 1
                'Debug.Print ndxAttempts;
            
            Case 0 ' a match
                lngSmaller = lngLarger 'satisfies loop
                ElementExists = True
            
            Case 1 'Element > Array middle
                '   Adjust the block boundaries
                lngSmaller = lngMiddle + 1
                ndxAttempts = ndxAttempts + 1
                'Debug.Print ndxAttempts;
        
        End Select
    Loop
    'Debug.Print String(ndxAttempts, "-") & "*"
Trap:

End Function

Public Function DataExtractAtomsSlow() As Boolean
    '   Seven times slower
    Dim oFile As clsFile
    Dim ndx As Long
    Dim lngCount As Long
    Dim lngLength As Long
    Dim strAtom As String
    Dim strTemp As String
    Const cDelimiter = vbCrLf
    
    '   Forward-reductively iterate through all delimited Pairs
    '   For each Atom, remove all subsequent instances of the Atom.
    '   The number of reduction-iterations will exactly-maybe equal the number of unique Atoms - 1.
    
    
    On Error GoTo Trap
    
    
    '   Clear Atoms buffer and copy the Pairs to
    '   strTemp for reduction searches
    mstrAtomsStringBuffer = ""
    strTemp = mstrAtomsStringBuffer & cDelimiter  '   append cDelimiter to the end of the strTemp,
                                            '   enabling pattern matching using the cDelimiter
                                            '   including the last atom in the strTemp string
    
    '   Flatten the delimiters to one
    '   Replace CRLF and "**" with cDelimiter
    strTemp = Replace(strTemp, "**", cDelimiter)
    strTemp = Replace(strTemp, vbCrLf, cDelimiter)
    'Debug.Print Left$(strTemp, 120)

    '   Loop on shrinking length
    While Len(strTemp) > 2
        DoEvents 'yielding to other apps some time
        '   Locate first Atom
        ndx = InStr(1, strTemp, cDelimiter)  'point to 1st cDelimiter
        Select Case ndx
            Case 0
            Case Is > 0
                '   Concatenate the atom and delimiter onto the class field
                strAtom = Left$(strTemp, ndx - 1)
                mstrAtomsStringBuffer = strAtom & cDelimiter
                
                '   Remove all occurances of the Atom and trailing cDelimiter from strTemp
                strTemp = Replace(strTemp, strAtom & cDelimiter, "")
        End Select
        lngCount = lngCount + 1
        'Debug.Print lngCount
    Wend
    
    '   The reduced set of Pairs is now a unique set of delimited Atoms in mstrAtomsStringBuffer.
    DataExtractAtomsSlow = True

ExitPoint:
    Exit Function

Trap:
    'Append contextual information into existing err object Description
    Err.Description = "SLIP.clsSLIP.DataExtractAtoms: " & Err.Description
    Err.Raise Err.Number
    
    Resume ExitPoint
End Function


Public Function DataImportPairs(ByVal Filespec As String) As Boolean
    '   Pairs.txt will always be expected in the DataPath
    '   Clear existing Document (object model)
    '   Load the Pairs into the root buffer
    '   Split the buffer into the PairsArray
    
    Dim ndx As Long
    Dim varX As Variant
    Dim strTemp As String
    
    On Error Resume Next
        
    mintMickey = Screen.MousePointer
    Screen.MousePointer = vbHourglass
    
    RaiseEvent Progress("Importing pair data from " & mstrDataPath & "\Paired.txt...")
    DoEvents
    
    Filespec = mstrDataPath & "\Paired.txt"
    '   Pairs.txt will always be expected in the DataPath
    With New clsFile
        Select Case .FileExists(Filespec)
        
            Case True
            
                '   Load the Pairs into the root buffer
                With New clsFile
                    mstrPairsStringBuffer = modMain.TrimTrailingCharacters(.ReadAll(Filespec), vbCrLf)
                End With
            
                '   Parse the PairNames into the class-level string array (module scope)
                mstrPairsArray = Split(Trim(mstrPairsStringBuffer), vbCrLf, compare:=vbBinaryCompare)
                                
            Case False
            
                MsgBox "Pairs file not found: " & Filespec & vbCrLf & vbCrLf & " Import aborted.", vbInformation
                
        End Select
    End With 'New clsFile
    
    '   Return success
    DataImportPairs = True
    mbooPairsImported = True

    RaiseEvent CommandResponse("Pairs Imported: " & CStr(UBound(mstrPairsArray) + 1) & " from " & Filespec)
    RaiseEvent Progress("")
    
    Screen.MousePointer = mintMickey
    Exit Function

Trap:
    RaiseEvent CommandResponse("Failure importing pair data from " & Filespec)
End Function

Public Property Let DataIterationEnabled(ByVal Value As Boolean)
    mbooIterating = Value
    RaiseEvent CommandResponse("Iteration " & IIf(mbooIterating, "enabled", "disabled"))
End Property

Public Property Get DataIterationEnabled() As Boolean
    DataIterationEnabled = mbooIterating
End Property

Public Function DataLoadMembers(Optional ByVal MembersFileSpec As String) As clsAtoms
    Dim strSplit() As String
    Dim ndx As Integer
    
    On Error Resume Next
    
    mintMickey = Screen.MousePointer
    Screen.MousePointer = vbHourglass
    
    With New clsFile
        .Filespec = MembersFileSpec
        
        '   Deserialize and return the file contents as a clsAtoms instance
        Set DataLoadMembers = Tabular.DeserializeMembers(.ReadAll) 'returns clsAtoms
    End With

    Screen.MousePointer = mintMickey

End Function


Public Sub DataLoadNestedTopics(ByVal Name As String, Optional ByRef oParent As clsTopic)
    '   Construct a Topic and for every sub-folder found, make the topic name the folder name
    '   Construct a subtopic by recursing this function, passing a topic reference as a parent
    '   with a the folder name appended to the TopicPath
    ' Note: this function calls itself for recursing subfolders.
    
    Dim topicX As clsTopic 'stores reference to new Topic node instance
    Dim intLevelDepth '  For each recursive entrance in this procedure, this stores the tree-depth
    
    Dim strDir As String
    Dim strTopicFolder As String
    Dim strFolderItem As String
    Dim strSubFolders As String 'delimited concatenation
    Dim SplitSubFolders() As String 'array
    
    Dim varX As Variant 'for-each enumerator index
        
    On Error Resume Next
                
    '   Determine depth into tree by counting ancestors
    
    Select Case (oParent Is Nothing) '   Boolean test
        Case True ' Test reveals a root topic, the top Topic instance, not having a parent
            '   Reset the LevelCounts for a top node
            ReDim mintLevelCount(0)
            mintLevelCount(0) = 1 'the top node will always be just one
            intLevelDepth = 0
            '   Create a top Topic class instance
            '   Note: the Topic instance creates all folders and files.
            Set topicX = Construct.CreateTopTopic( _
               Name, _
               cDefPlotTopX, _
               cDefPlotTopY)
        Case False '    a non-root topic, having a parent
            intLevelDepth = UBound(Split(oParent.Path, "\"))
            ReDim Preserve mintLevelCount(intLevelDepth + 1)
            '   Increment a topic graph node count at this level
            mintLevelCount(intLevelDepth) = mintLevelCount(intLevelDepth) + 1
            '   Create, set properties and set the parent of a new Topic
            Set topicX = Construct.CreateTopic( _
               Name:=Name, _
               oParent:=oParent, _
               x:=mintLevelCount(intLevelDepth) * 500, _
               Y:=oParent.MetricsY + 700)
    End Select
    
    '   Enumerate all folders in the topic folder, and create a child Topic
    '   for each folder found by recursing to this function
    '   with the topicX parameter as oParent
    
    '   Build a string holding the path to the topic folder
    strTopicFolder = mstrDataPath & topicX.Path & "\"   ' Set the path.
    
    '   Read the folder for subfolders
    strFolderItem = Dir(strTopicFolder, vbDirectory)    '   Retrieve the first entry.
    
    Dim x As String
    '   Concatenate a tabular list of sub folders in the Topic folder, pipe delimited
    Do While strFolderItem <> ""   ' Start the loop.
        '   Ignore the current directory and the encompassing directory.
        If strFolderItem <> "." And strFolderItem <> ".." Then
            
            '   Use bit-masking to test for a directory.
            If (GetAttr(strTopicFolder & strFolderItem) And vbDirectory) = vbDirectory Then
                ' Concatenate the folder name with a pipe
                strSubFolders = strSubFolders & strFolderItem & "|"
            End If
        End If
        strFolderItem = Dir    ' Get next entry.
        'Debug.Print strFolderItem, strSubFolders
    Loop
    
    '   Remove trailing delimiter
    If Len(strSubFolders) > 0 Then
        strSubFolders = Left$(strSubFolders, Len(strSubFolders) - 1)
    End If
    
    '   Enumerate the tabulation of folders, and create a sub-folder for each
    SplitSubFolders = Split(strSubFolders, "|")
    
    'Debug.Print topicX.Path
    
    For Each varX In Split(strSubFolders, "|")
        '   Recursion happens here
        '   Create a child Topic on topicX with the folder name in strFolderItem
        '   not working when nested...Dir statement can't terminate by ever returning null.
           DataLoadNestedTopics varX, topicX
    Next
   
End Sub

Public Sub DataRandomizeAtoms(ByRef oTopic As clsTopic, ByVal MinMax As Long)
    '   Random spatial assignment within a cubical volume (X,Y,Z).
    Dim atomX As clsAtom
    
    '   Reset iteration count
    oTopic.RadialIters = 0
    
    For Each atomX In oTopic.Atoms
        '   Radial assignment
        atomX.Degree = CInt(Rnd * 360) + 1
        
        '   Random spatial assignment within a cubical volume (X,Y,Z).
        atomX.MetricsX = CInt(Rnd * MinMax) - MinMax / 2 + 400
        atomX.MetricsY = CInt(Rnd * MinMax) - MinMax / 2 + 400
        atomX.MetricsZ = CInt(Rnd * MinMax) - MinMax / 2 + 400
    Next
    
    '   Save the Members tabulation to file
    With New clsFile
        
        '   Convert the Atoms object to a string
        
        
        '   Save the tabulation of the current iterated state of members
        .Save _
                Buffer:=Tabular.SerializeMembers(oTopic.Atoms), _
                Filespec:=mstrDataPath & oTopic.Path & "\Members.txt"
        .CloseFileWrite
        
                '   Send notifacation message
        RaiseEvent AtomRadialDistributionChanged(oTopic)
    End With
    
    
End Sub

Public Function DataSaveMembers(ByRef oAtoms As clsAtoms) As Boolean
    On Error GoTo Trap
    
    With New clsFile
        .Save DataAtomsStringBuffer, mstrDataPath & "\Members.txt"
    End With
    
    DataSaveMembers = True
    Exit Function
    
Trap:
End Function

Public Function MetricsAtomicDistance(AtomA As clsAtom, AtomB As clsAtom) As Long
    MetricsAtomicDistance = Abs(( _
                (AtomB.MetricsX - AtomA.MetricsX) ^ 3 + _
                (AtomB.MetricsY - AtomA.MetricsY) ^ 3 + _
                (AtomB.MetricsZ - AtomA.MetricsZ) ^ 3 _
                ) ^ (1 / 3))
End Function

Public Function MetricsTopicDistance(Topic1 As clsTopic, Topic2 As clsTopic) As Long
    MetricsTopicDistance = Abs(( _
                (Topic2.MetricsX - Topic1.MetricsX) ^ 3 + _
                (Topic2.MetricsY - Topic1.MetricsY) ^ 3 + _
                (Topic2.MetricsZ - Topic1.MetricsZ) ^ 3 _
                ) ^ (1 / 3))
End Function

Public Property Get metricsLabelWidth() As Long
    metricsLabelWidth = 2000
End Property

Public Property Get metricsPlotSize() As Long
    metricsPlotSize = 4695
End Property
Public Property Get MetricsRadius() As Long
    MetricsRadius = 1000
End Property
Public Property Get metricsTopX() As Long
    metricsTopX = 500
End Property
Public Property Get metricsTopY() As Long
    metricsTopY = 500
End Property

Public Property Get metricsWhiteSpace() As Long
    metricsWhiteSpace = 60
End Property

Public Function TabularDeserializeMembers(ByVal Members As String) As clsAtoms
    '   Transform a serialized string to class instance
    
    Dim strOuterSplit() As String 'array
    Dim strInnerSplit() As String 'array
    Dim strAtom As String
    Dim oAtom As clsAtom
    Dim oMembers As clsAtoms
    Dim ndx, lastNdx As Integer
    Const cOuterDelimiter = vbCrLf
    
    On Error Resume Next
    
    '   Split the members string into an array
    strOuterSplit = Split(Members, cOuterDelimiter)
    
    '   Store the index of the last array element
    lastNdx = UBound(strOuterSplit)
    
    '   Instance an Atoms class, the return object
    Set oMembers = New clsAtoms
    
    '    Enumerate the split array
    For ndx = 0 To lastNdx
        '   Instance a new atom
        With New clsAtom
            strInnerSplit = Split(strOuterSplit(ndx), vbTab)
            If UBound(strInnerSplit) = 1 Then  '
                .Name = strInnerSplit(0) 'left of inner delimiter
                .Degree = CInt(strInnerSplit(1)) 'right of inner delimiter
                
                '   Add a new reference in the collection class of the parent object
                If Len(.Name) Then
                    oMembers.VBACollection.Add Item:=.Self, Key:=.Name
                End If
            End If
        End With
    Next
    
    Set TabularDeserializeMembers = oMembers
    
    Set oMembers = Nothing
    Set oAtom = Nothing
End Function

Public Function TabularMembersStringToRadialsString(ByVal Members As String) As String
    '   Input: Name Tab Degree CR
    '   Output: RadialAngle Tab Count CR
    '   The output contains 360 Radial counts, or sum of members per degree
    
    Dim intDegreeCounts(360) As Integer 'ignore zero base element
    Dim strOuterSplit() As String 'array
    Dim intDegree As Integer
    Dim ndx As Integer
    Dim intLast As Integer
    Const cOuterDelimiter = vbCrLf
    Const cInnerDelimiter = vbTab
    
    On Error Resume Next
    
    '   Split the members string into an array
    strOuterSplit = Split(Members, cOuterDelimiter)
    
    '   Store the last array element index
    intLast = UBound(strOuterSplit)
    
    '    Enumerate the split array summing counts of Members per degree
    For ndx = 0 To intLast
        '   Get the degree by splitting the member, and accessing the 2nd element (1)
        intDegree = CInt(Split(strOuterSplit(ndx), cInnerDelimiter)(1)) 'left element of split on outer split, the member degree
        
        '   Sum the count per degree
        intDegreeCounts(intDegree) = intDegreeCounts(intDegree) + 1
    Next
    
    '   Concatenate the output text - RadialAngle Tab Count CR
    For ndx = 1 To 360
        TabularMembersStringToRadialsString = TabularMembersStringToRadialsString & _
                    CStr(ndx) & vbTab & CStr(intDegreeCounts(ndx)) & vbCrLf
    Next
End Function

Public Function TabularSerializeMembers(ByRef Atoms As clsAtoms) As String
    Dim atomX As clsAtom
    Dim ndx As Integer

    On Error Resume Next
    
    '   Concatenate Members as - Name Tab Degree CR
    For Each atomX In Atoms
        TabularSerializeMembers = TabularSerializeMembers & atomX.Name & vbTab & CStr(atomX.Degree) & vbCrLf
    Next
End Function

Public Function TabularSerializeRadials(ByRef Atoms As clsAtoms) As String
    Dim atomX As clsAtom
    Dim intDegreeCounts(360) As Integer 'ignore zero base element
    Dim ndx As Integer
    
    On Error Resume Next
    
    '   Sum count of Atoms per degree
    For Each atomX In Atoms
        intDegreeCounts(atomX.Degree) = intDegreeCounts(atomX.Degree) + 1
    Next
    
    '   Concatenate Degree Tab Count CR
    For ndx = 1 To 360
        TabularSerializeRadials = _
            TabularSerializeRadials & CStr(ndx) & vbTab & CStr(intDegreeCounts(ndx)) & vbCrLf
    Next
End Function

Public Property Get DataPath() As String
    If Len(DataPath) Then mstrDataPath = DataPath Else DataPath = cDefDataPath
    
End Property
Public Property Let DataPath(ByVal Value As String)
    mstrDataPath = Value
End Property

Public Property Get EventBrowserExecName() As String
    EventBrowserExecName = mstrEventBrowserExecName
End Property
Public Property Let EventBrowserExecName(ByVal Value As String)
    mstrEventBrowserExecName = Value
End Property
 
 
Public Property Get TopicSelection() As clsTopic
    On Error Resume Next
    Set TopicSelection = moTopicSelection
End Property

Public Property Set TopicSelection(ByVal ref As clsTopic)
    On Error Resume Next
    Set moTopicSelection = ref
End Property

Public Property Get TopicCount() As Long
    TopicCount = mcolTopicTree.Count
End Property