using Adaptation.Shared; using Adaptation.Shared.Methods; using log4net; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Adaptation.FileHandlers.txt; public partial class ProcessData { internal static List> GetTuples(FileRead fileRead, Logistics logistics, List fileInfoCollection, string originalDataBioRad) { List> results = new(); ILog log = LogManager.GetLogger(typeof(ProcessData)); // *********************************************************************************** // * Step #2 - Verify completeness of each cassette scan in the raw data source file * // *********************************************************************************** string line; StreamReader rawDataFilePtr; bool? cassetteScanCompleted = null; // Scrub the source file to verify that for each cassette, present in the file, there is a complete // data set (i.e., that is there is a start and finished statement). // // Scenario #1 - Normal // For every cassette "started" there must be a matching cassette "finished". // Scenario #2 - Only Cassette "finished" (with or witout additional cassette complete data sets) // Incomplete data file. File will be process and generate error for the incomplete portion. // Scenario #3 - Only Cassette "Started" // Bail out of the solution. Source data file not ready to be processed. using (rawDataFilePtr = new StreamReader(logistics.ReportFullPath)) { for (short i = 0; i < short.MaxValue; i++) { line = rawDataFilePtr.ReadLine(); if (line is null) break; if (line.Contains("Cassette") && line.Contains("started") && (cassetteScanCompleted is null || cassetteScanCompleted.Value)) { cassetteScanCompleted = false; log.Debug("****Extract() - CassetteScanCompleted = FALSE"); } else if (line.Contains("Cassette") && line.Contains("finished") && (cassetteScanCompleted is null || !cassetteScanCompleted.Value)) { cassetteScanCompleted = true; log.Debug("****Extract() - CassetteScanCompleted = TRUE"); } } // Making sure that the file has been released rawDataFilePtr.Close(); rawDataFilePtr?.Dispose(); } if (cassetteScanCompleted is null || !cassetteScanCompleted.Value) // Raw source file has an incomplete data set or it only contains a "Process failed" and should not be // processed /split yet. Simply get out of this routine until enough data has been appended to the file. log.Debug($"****Extract() - Raw source file has an incomplete data set and should not be processed yet."); else { Dictionary> cassetteIDAndDataSets = new(); if (!string.IsNullOrEmpty(logistics.ReportFullPath)) { string[] segments; int cassetteEndIndex; int thicknessCounter; string thicknessHead; string thicknessInfo; string thicknessTail; int cassetteStartIndex; StringBuilder lines = new(); string slotID = string.Empty; string cassetteID = string.Empty; string batchHeader = string.Empty; bool finishedReadingThicknessInfo; bool slotInformationCaptured = false; bool pointsInformationCaptured = false; bool sourceInformationCaptured = false; bool waferWaferInformationCaptured = false; bool destinationInformationCaptured = false; string[] reportFullPathlines = File.ReadAllLines(logistics.ReportFullPath); List> cassetteStartAndEnds = new(); for (int i = 0; i < reportFullPathlines.Length; i++) { line = reportFullPathlines[i].Trim(); if (string.IsNullOrEmpty(line)) continue; if (line.StartsWith("Batch") && line.Contains("started")) batchHeader = line; if (i + 1 == reportFullPathlines.Length) continue; if (line.StartsWith("Cassette") && line.Contains("started")) { for (int j = i + 1; j < reportFullPathlines.Length; j++) { if (j + 1 == reportFullPathlines.Length) cassetteStartAndEnds.Add(new Tuple(batchHeader, i, j)); else { line = reportFullPathlines[j].Trim(); if (line.StartsWith("Cassette") && line.Contains("started")) { cassetteStartAndEnds.Add(new Tuple(batchHeader, i, j - 1)); break; } } } } } foreach (Tuple tuple in cassetteStartAndEnds) { _ = lines.Clear(); batchHeader = tuple.Item1; cassetteEndIndex = tuple.Item3; cassetteStartIndex = tuple.Item2; for (int l = cassetteStartIndex; l <= cassetteEndIndex; l++) { line = reportFullPathlines[l].Trim(); if (string.IsNullOrEmpty(line)) continue; if (l == cassetteStartIndex) { // Save the previously saved "Batch Header" _ = lines.AppendLine(batchHeader); // Save the first line of the cassette scan information _ = lines.AppendLine(line); // Each new cassette initialize the WaferWafer information flag waferWaferInformationCaptured = false; slotInformationCaptured = false; if (line.Length > 9) { // Detected a new cassette data scan. Extract the cassette ID. // Example: "Cassette 47-241330-4238 started." segments = line.Substring(9).Split(new string[] { "started" }, StringSplitOptions.RemoveEmptyEntries); if (segments.Any()) { // Detected a new cassette scan in the raw source file cassetteID = segments[0].Trim(); cassetteID = cassetteID.Replace(":", string.Empty); cassetteID = cassetteID.Replace("*", string.Empty); cassetteID = cassetteID.Replace("\\", string.Empty); } } } // Continue reading and saving the cassette scan information, into the cassette // scan output file, until the end of the cassette scan "Finished" statement has // been detected. // Maintain standard for mat between various BioRad tools. The "Points" and "Thickness" // values between various BioRad tools might be spread over multiple lines. The following // is simply to regroup the "Points" and "Thickness" information on the same line accordingly. if (line.StartsWith("Wafer Wafer")) { _ = lines.AppendLine(line); slotInformationCaptured = false; waferWaferInformationCaptured = true; } else if (line.StartsWith("Slot")) { slotID = string.Empty; segments = line.Split(' '); if (segments.Length > 1) slotID = segments[1]; // There are cases where the WaferWafer information is missing. Create a // WaferWafer entry based off the slot number. if (!waferWaferInformationCaptured) { waferWaferInformationCaptured = true; _ = lines.AppendLine("Wafer Wafer " + slotID + "."); } _ = lines.AppendLine(line); slotInformationCaptured = true; } else if (line.StartsWith("Recipe")) { _ = lines.AppendLine(line); pointsInformationCaptured = false; } else if (line.StartsWith("Points")) { _ = lines.AppendLine(line); pointsInformationCaptured = true; } else if (line.Contains("Thickness")) { // Before addressing the "Thickness" section, ensure that the "Points" section // has been found. Otherwise, we need to write out a default value. if (!pointsInformationCaptured) { // No "Points" information has been capture. Default to "Points : 0 0" _ = lines.AppendLine("Points : 0 0"); pointsInformationCaptured = true; } // The "Thickness" output section comes out differently between various Stratus tools. In some // cases, the thickness values are either empty (no values), on the same line or on different lines. // Below are examples of how the data needs to be formatted after being parsed: // Thickness, um 1 - 1 0 // Thickness, um 1 - 1 13.630 // Thickness, um 1 - 9 1.197 1.231 1.248 1.235 1.199 1.202 1.236 1.242 1.212 thicknessCounter = 0; thicknessHead = line; thicknessInfo = ""; thicknessTail = ""; finishedReadingThicknessInfo = false; for (int t = l + 1; t <= cassetteEndIndex; t++) { l = t; line = reportFullPathlines[l].Trim(); if (string.IsNullOrEmpty(line)) continue; if (!line.StartsWith("Slot")) { thicknessCounter++; thicknessTail = string.Concat(thicknessTail, " ", line); } else { finishedReadingThicknessInfo = true; if (thicknessCounter != 0) thicknessInfo = string.Concat(" 1 - ", thicknessCounter); else { // Two possible formatting scenarios at this point. Either the data was already // formatted properly on one line. Or the Thickness value was missing, in which // case we need to default the thickness value to zero (0). segments = thicknessHead.Split(' '); if (segments.Length > 2) { // The "Thickness" raw data if formatted as a normal single line format and // already include the Header + Info + Tail } else { // The "Thickness raw data has no values. Formatting the output with zero. thicknessInfo = " 1 - 1"; thicknessTail = " 0"; } } _ = lines.AppendLine(string.Concat(thicknessHead, thicknessInfo, thicknessTail)); // The "Slot" keyword is the tag that determines the end of the Thickness section. The "Slot" // information has already been ready. Simply write it back. _ = lines.AppendLine(line); } if (finishedReadingThicknessInfo) break; } } else if (line.StartsWith("Mean")) { _ = lines.AppendLine(line); sourceInformationCaptured = false; destinationInformationCaptured = false; } else if (line.StartsWith("Source:") && slotInformationCaptured) { _ = lines.AppendLine(line); sourceInformationCaptured = true; } else if (line.StartsWith("Destination:") && slotInformationCaptured) { if (!sourceInformationCaptured) { sourceInformationCaptured = true; _ = lines.AppendLine(string.Concat("Source: Slot ", slotID, ", Cassette")); } _ = lines.AppendLine(line); destinationInformationCaptured = true; // Each time a cassette slot section has been completed, we must reinitialize // the "Wafer Wafer" information flag in case there are multiple slots in the // same cassette slotInformationCaptured = false; waferWaferInformationCaptured = false; } else if (line.StartsWith("Cassette") && line.Contains("finished.")) { // Reach the end of the cassette data set information if (!sourceInformationCaptured) { sourceInformationCaptured = true; _ = lines.AppendLine(string.Concat("Source: Slot ", slotID, ", Cassette")); } if (!destinationInformationCaptured) { destinationInformationCaptured = true; _ = lines.AppendLine(string.Concat("Destination: Slot ", slotID, ", Cassette")); // Each time a cassette slot section has been completed, we must reinitialize // the "Wafer Wafer" information flag in case there are multiple slots in the // same cassette slotInformationCaptured = false; waferWaferInformationCaptured = false; } // Write the end of cassette statement to the output file _ = lines.AppendLine(line); // Read the Mean-Average line information, post the cassette "Finished" statement for (int a = l + 1; a <= cassetteEndIndex; a++) { l = a; line = reportFullPathlines[l].Trim(); if (string.IsNullOrEmpty(line)) continue; // There are many blank lines in the source file. Search for the first // occurrence of the string "Mean". if (line.StartsWith("Mean")) { _ = lines.AppendLine(line); break; } // The mean Average information is missing. We are done reading the cassette information. if (line.StartsWith("Batch")) break; } if (!cassetteIDAndDataSets.ContainsKey(cassetteID)) cassetteIDAndDataSets.Add(cassetteID, new List()); cassetteIDAndDataSets[cassetteID].Add(lines.ToString()); } } } if (cassetteStartAndEnds is null) { } } if (cassetteIDAndDataSets.Any()) { int wafer; string user; string runID; bool isBioRad; string recipe; int count = -1; int stringIndex; string dataText; string dataType; string[] segments; string cassetteID; string recipeName; IProcessData iProcessData; DateTime cassetteDateTime; string recipeSearch = "Recipe"; string toolType = string.Empty; StringBuilder contents = new(); Stratus.ProcessData processData; foreach (KeyValuePair> keyValuePair in cassetteIDAndDataSets) { isBioRad = false; dataType = string.Empty; cassetteID = keyValuePair.Key; for (int i = 0; i < keyValuePair.Value.Count; i++) { dataText = keyValuePair.Value[i]; // Finished capturing the complete cassette scan data information. Release the cassette file. if (dataText.Contains("Cassette") && dataText.Contains("Wafer") && dataText.Contains("Slot") && dataText.Contains("Recipe") && dataText.Contains("Points") && dataText.Contains("Thickness") && dataText.Contains("Mean") && dataText.Contains("Source:") && dataText.Contains("Destination:")) { // Extract the recipe name runID = string.Empty; recipeName = string.Empty; stringIndex = dataText.IndexOf(recipeSearch); recipeName = dataText.Substring(stringIndex + recipeSearch.Length); log.Debug($"****Extract(FDR): recipeName = {recipeName}"); #pragma warning disable CA2249 if (!string.IsNullOrEmpty(recipeName) && (recipeName.IndexOf("center", StringComparison.CurrentCultureIgnoreCase) >= 0)) #pragma warning restore CA2249 { /***************************************/ /* STRATUS Measurement = FQA Thickness */ /***************************************/ // Recipes that contains the substring "Center" are STRATUS centerpoint recipes. They are used for Inspection and FQA measurements. // measurement. The data from these scans should be uploaded to the Metrology Viewer database as STRATUS and uploaded to the // OpenInsight [FQA Thickness - Post Epi - QA Metrology / Thk/RHO Value for each slotID] automatically. isBioRad = false; toolType = "STRATUS"; dataType = "FQA Thickness"; } #pragma warning disable CA2249 else if (!string.IsNullOrEmpty(recipeName) && (recipeName.IndexOf("prod_", StringComparison.CurrentCultureIgnoreCase) >= 0)) #pragma warning restore CA2249 { /******************************************/ /* BIORAD Measurement = Product Thickness */ /******************************************/ // Recipes that contains the substring "Center" are STRATUS centerpoint recipes. They are used for Inspection and FQA measurements. // measurement. The data from these scans should be uploaded to the Metrology Viewer database as STRATUS and uploaded to the // OpenInsight [FQA Thickness - Post Epi - QA Metrology / Thk/RHO Value for each slotID] automatically. isBioRad = true; toolType = "BIORAD"; dataType = "Product Thickness"; } else if (!string.IsNullOrEmpty(recipeName) && #pragma warning disable CA2249 ((recipeName.IndexOf("T-Low", StringComparison.CurrentCultureIgnoreCase) >= 0) || (recipeName.IndexOf("T_Low", StringComparison.CurrentCultureIgnoreCase) >= 0) || (recipeName.IndexOf("T-Mid", StringComparison.CurrentCultureIgnoreCase) >= 0) || (recipeName.IndexOf("T_Mid", StringComparison.CurrentCultureIgnoreCase) >= 0) || (recipeName.IndexOf("T-High", StringComparison.CurrentCultureIgnoreCase) >= 0) || (recipeName.IndexOf("T_High", StringComparison.CurrentCultureIgnoreCase) >= 0))) #pragma warning restore CA2249 { /*************************************/ /* BIORAD Measurement = No Uploading */ /*************************************/ // Recipes that contains the substring "T-Low, T_Low, T-Mid, T_Mid and T-High, T_High" are BIORAD verification recipe. The information // should be uploaded to the Metrology Viewer database as BIORAD. No OpenInsight. isBioRad = true; toolType = "BIORAD"; dataType = "Verification"; } else { // Count the number of wafers (ref. "Source: Slot") in the cassette int waferCount = Regex.Matches(dataText, "Source: Slot").Count; if (waferCount == 1) { // Metrology Thickness. Upload to OpenInsight same as BR2 and BR3 isBioRad = true; toolType = "BIORAD"; dataType = "Metrology Thickness"; } else if (waferCount > 1) { // Inspection Measurement. Do not upload to OpenInsight. isBioRad = true; toolType = "BIORAD"; dataType = "Inspection"; } } } log.Debug($"****Extract(FDR): ToolType = {toolType}"); log.Debug($"****Extract(FDR): DataType = {dataType}"); if (!isBioRad) { cassetteDateTime = logistics.DateTimeFromSequence.AddTicks(i * -1); results.Add(new Tuple(cassetteID, isBioRad, cassetteDateTime, dataText)); } else { processData = new Stratus.ProcessData(fileRead, logistics, fileInfoCollection, originalDataBioRad, dataText: dataText); iProcessData = processData; if (!iProcessData.Details.Any()) log.Warn("No Details!"); else { foreach (object item in iProcessData.Details) { if (item is not Stratus.Detail detail) throw new Exception(); count += 1; _ = contents.Clear(); cassetteDateTime = logistics.DateTimeFromSequence.AddTicks(count * -1); user = processData.Employee?.ToString() ?? ""; recipe = detail.Recipe?.ToString() ?? ""; _ = contents.Append("Bio-Rad ").Append("QS400MEPI".PadRight(17)).Append("Recipe: ").Append(recipe.PadRight(25)).AppendLine(processData.Date.ToString(Stratus.Description.GetDateFormat())); _ = contents.Append("operator: ").Append(user.PadRight(22)).Append("batch: BIORAD #").AppendLine(logistics.JobID.Substring(6, 1)); _ = contents.Append("cassette: ").Append("".PadRight(22)).Append("wafer: ").AppendLine(processData.Cassette); _ = contents.AppendLine("--------------------------------------------------------------------------------"); _ = contents.AppendLine(" position thickness position thickness position thickness"); segments = detail.Thickness.Split(','); for (int j = 0; j < segments.Length; j++) { wafer = j + 1; _ = contents.Append(wafer.ToString().PadLeft(11)); if ((wafer % 3) > 0) _ = contents.Append(segments[j].PadLeft(10)); else _ = contents.AppendLine(segments[j].PadLeft(10)); } if ((segments.Length % 3) > 0) _ = contents.AppendLine(); _ = contents.Append(" wafer mean thickness = ").Append(detail.Mean).Append(", std. dev = ").Append(detail.StdDev).Append(' ').AppendLine(detail.PassFail); _ = contents.AppendLine("================================================================================"); _ = contents.AppendLine(""); _ = contents.AppendLine("Radial variation (computation B) PASS:"); _ = contents.AppendLine(""); _ = contents.AppendLine(" thickness 0.0000"); results.Add(new Tuple(cassetteID, isBioRad, cassetteDateTime, contents.ToString())); } } } } } } } // ********************************************** // * Step #3 - Protect the raw data source file * // ********************************************** // The multi-cassettes raw source file is ready to be splitted. Each cassette scan set has // been determined to be complete (i.e., has the started & finished statements). At this point // it is important to rename the multi-cassette raw data source file, located in the RawData // folder, to a different name so that the tool does not attempt to update the file while being // processed by the EAF cell instance. // Get the last date/time the DataBioRad.txt file was updated DateTime afterCheck = File.GetLastWriteTime(logistics.ReportFullPath); // Ensure that the DataBioRad.txt file has not been updated since the FileReader began the healthcheck // If the date/time values are different between the "Before" and "After" checks then let it go. The // tool is still busy trying to update the file. The FileReader will try to catch the data on the // next update. if (logistics.DateTimeFromSequence != afterCheck) { results.Clear(); log.Debug($"****Extract() - DataBioRad.txt file is getting updated fast"); log.Debug($"****Extract() - DataBioRadDateTime_AfterCheck = {afterCheck.Ticks}"); log.Debug($"****Extract() - DataBioRadDateTime_BeforeCheck = {logistics.Sequence}"); } return results; } }