Beispiel #1
0
        static void Main(string[] args)
        {
            // parse command line arguments
            var options = new Options();

            if (!CommandLine.Parser.Default.ParseArgumentsStrict(args, options))
            {
                Console.WriteLine("*** ParseArgumentsStrict returned false");
            }
            if (options.BeQuiet == true)
            {
                ConsoleUI.BeSilent();
            }
            else
            {
                ConsoleUI.BeVerbatim();
            }
            ConsoleUI.Welcome();

            // get the filename(s)
            string[] fileNames = options.ListOfFileNames.ToArray();
            if (fileNames.Length == 0)
            {
                ConsoleUI.ErrorExit("!Missing input file", 1);
            }
            // read all relevant scan data
            ConsoleUI.StartOperation("Reading and evaluating files");
            NmmFileName nmmFileName = new NmmFileName(fileNames[0]);

            nmmFileName.SetScanIndex(options.ScanIndex);
            NmmScanData nmmScanData = new NmmScanData(nmmFileName);

            ConsoleUI.Done();

            if (options.DoHeydemann)
            {
                nmmScanData.ApplyHeydemannCorrection();
                if (nmmScanData.HeydemannCorrectionApplied)
                {
                    ConsoleUI.WriteLine($"Heydemann correction applied, span {nmmScanData.HeydemannCorrectionSpan * 1e9:F1} nm");
                }
                else
                {
                    ConsoleUI.WriteLine($"Heydemann correction not successful.");
                }
            }

            // some checks of the provided CLA options
            if (options.ProfileIndex < 0)
            {
                options.ProfileIndex = 0;
            }
            if (options.ProfileIndex > nmmScanData.MetaData.NumberOfProfiles)
            {
                options.ProfileIndex = nmmScanData.MetaData.NumberOfProfiles;
            }

            TopographyProcessType topographyProcessType = TopographyProcessType.ForwardOnly;

            if (options.UseBack)
            {
                topographyProcessType = TopographyProcessType.BackwardOnly;
            }
            if (options.UseBoth)
            {
                topographyProcessType = TopographyProcessType.Average;
            }
            if (options.UseDiff)
            {
                topographyProcessType = TopographyProcessType.Difference;
            }
            if (nmmScanData.MetaData.ScanStatus == ScanDirectionStatus.ForwardOnly)
            {
                if (topographyProcessType != TopographyProcessType.ForwardOnly)
                {
                    ConsoleUI.WriteLine("No backward scan data present, switching to forward only.");
                }
                topographyProcessType = TopographyProcessType.ForwardOnly;
            }
            if (nmmScanData.MetaData.ScanStatus == ScanDirectionStatus.Unknown)
            {
                ConsoleUI.ErrorExit("!Unknown scan type", 2);
            }
            if (nmmScanData.MetaData.ScanStatus == ScanDirectionStatus.NoData)
            {
                ConsoleUI.ErrorExit("!No scan data present", 3);
            }

            // now we can start to sort and format everything we need
            BcrWriter bcr = new BcrWriter();

            bcr.Relaxed = !options.Strict; // overrules Relaxed
            ConsoleUI.WriteLine(bcr.Relaxed ? "Relaxed formatting" : "Strict formatting");
            bcr.ForceIsoFormat = options.IsoFormat;
            ConsoleUI.WriteLine(bcr.ForceIsoFormat ? "ISO 25178-71 format" : "Legacy format");

            // ISO 25178-71 file header
            bcr.CreationDate             = nmmScanData.MetaData.CreationDate;
            bcr.ManufacurerId            = nmmScanData.MetaData.InstrumentIdentifier;
            bcr.NumberOfPointsPerProfile = nmmScanData.MetaData.NumberOfDataPoints;
            if (options.ProfileIndex == 0)
            {
                bcr.NumberOfProfiles = nmmScanData.MetaData.NumberOfProfiles;
                bcr.YScale           = nmmScanData.MetaData.ScanFieldDeltaY;
                ConsoleUI.WriteLine("Extract complete scanfield");
            }
            else
            {
                bcr.NumberOfProfiles = 1;
                bcr.YScale           = 0;
                ConsoleUI.WriteLine($"Extract single profile {options.ProfileIndex} only");
            }
            bcr.XScale = nmmScanData.MetaData.ScanFieldDeltaX;
            bcr.ZScale = options.ZScale;

            // read actual topography data for given channel
            if (!nmmScanData.ColumnPresent(options.ChannelSymbol))
            {
                ConsoleUI.ErrorExit($"!Channel {options.ChannelSymbol} not in scan data", 5);
            }
            double[] rawData = nmmScanData.ExtractProfile(options.ChannelSymbol, options.ProfileIndex, topographyProcessType);

            // level data
            DataLeveling levelObject;

            if (options.ProfileIndex == 0)
            {
                levelObject = new DataLeveling(rawData, nmmScanData.MetaData.NumberOfDataPoints, nmmScanData.MetaData.NumberOfProfiles);
            }
            else
            {
                levelObject = new DataLeveling(rawData, nmmScanData.MetaData.NumberOfDataPoints);
            }
            levelObject.BiasValue = options.Bias * 1.0e-6; //  bias is given in µm on the command line
            double[] leveledTopographyData = levelObject.LevelData(MapOptionToReference(options.ReferenceMode));

            // generate a dictionary with all relevant metadata for the ISO 25178-71 file trailer
            Dictionary <string, string> bcrMetaData = new Dictionary <string, string>();

            bcrMetaData.Add("InputFile", nmmScanData.MetaData.BaseFileName);
            bcrMetaData.Add("ConvertedBy", $"{ConsoleUI.Title} version {ConsoleUI.Version}");
            bcrMetaData.Add("UserComment", options.UserComment);
            bcrMetaData.Add("OperatorName", nmmScanData.MetaData.User);
            bcrMetaData.Add("Organisation", nmmScanData.MetaData.Organisation);
            bcrMetaData.Add("SampleIdentifier", nmmScanData.MetaData.SampleIdentifier);
            bcrMetaData.Add("SampleSpecies", nmmScanData.MetaData.SampleSpecies);
            bcrMetaData.Add("SampleSpecification", nmmScanData.MetaData.SampleSpecification);
            bcrMetaData.Add("SPMtechnique", nmmScanData.MetaData.SpmTechnique);
            bcrMetaData.Add("Probe", nmmScanData.MetaData.ProbeDesignation);
            bcrMetaData.Add("ZAxisSource", options.ChannelSymbol);
            bcrMetaData.Add("Trace", topographyProcessType.ToString());
            if (options.ProfileIndex != 0)
            {
                bcrMetaData.Add("NumProfiles", $"{nmmScanData.MetaData.NumberOfProfiles}");
                bcrMetaData.Add("ExtractedProfile", $"{options.ProfileIndex}");
            }
            if (nmmScanData.MetaData.NumberOfScans > 1)
            {
                bcrMetaData.Add("NumberOfScans", $"{nmmScanData.MetaData.NumberOfScans}");
                bcrMetaData.Add("Scan", $"{nmmScanData.MetaData.ScanIndex}");
            }
            bcrMetaData.Add("ReferenceDatum", levelObject.LevelModeDescription);
            if (nmmScanData.HeydemannCorrectionApplied)
            {
                bcrMetaData.Add("HeydemannCorrection", $"Span {nmmScanData.HeydemannCorrectionSpan * 1e9:F1} nm");
            }
            bcrMetaData.Add("EnvironmentMode", nmmScanData.MetaData.EnvironmentMode);
            bcrMetaData.Add("SampleTemperature", $"{nmmScanData.MetaData.SampleTemperature:F3} oC");
            bcrMetaData.Add("AirTemperature", $"{nmmScanData.MetaData.AirTemperature:F3} oC");
            bcrMetaData.Add("AirPressure", $"{nmmScanData.MetaData.BarometricPressure:F0} Pa");
            bcrMetaData.Add("AirHumidity", $"{nmmScanData.MetaData.RelativeHumidity:F1} %");
            bcrMetaData.Add("TemperatureGradient", $"{nmmScanData.MetaData.AirTemperatureGradient:F3} oC");
            bcrMetaData.Add("TemperatureRange", $"{nmmScanData.MetaData.AirTemperatureDrift:F3} oC");
            bcrMetaData.Add("ScanSpeed", $"{nmmScanData.MetaData.ScanSpeed * 1e6} um/s");
            bcrMetaData.Add("AngularOrientation", $"{nmmScanData.MetaData.ScanFieldRotation:F3} grad");
            bcrMetaData.Add("ScanFieldCenterX", $"{nmmScanData.MetaData.ScanFieldCenterZ * 1000:F3} mm");
            bcrMetaData.Add("ScanFieldCenterY", $"{nmmScanData.MetaData.ScanFieldCenterY * 1000:F3} mm");
            bcrMetaData.Add("ScanFieldCenterZ", $"{nmmScanData.MetaData.ScanFieldCenterZ * 1000:F3} mm");
            bcrMetaData.Add("ScanFieldOriginX", $"{nmmScanData.MetaData.ScanFieldOriginX} m");
            bcrMetaData.Add("ScanFieldOriginY", $"{nmmScanData.MetaData.ScanFieldOriginY} m");
            bcrMetaData.Add("ScanFieldOriginZ", $"{nmmScanData.MetaData.ScanFieldOriginZ} m");
            bcrMetaData.Add("ScanDuration", $"{nmmScanData.MetaData.ScanDuration.TotalSeconds:F0} s");
            bcrMetaData.Add("GlitchedDataPoints", $"{nmmScanData.MetaData.NumberOfGlitchedDataPoints}");
            bcrMetaData.Add("SpuriousDataLines", $"{nmmScanData.MetaData.SpuriousDataLines}");
            for (int i = 0; i < nmmScanData.MetaData.ScanComments.Count; i++)
            {
                bcrMetaData.Add($"ScanComment{i + 1}", nmmScanData.MetaData.ScanComments[i]);
            }

            // ISO 25178-71 main section
            bcr.PrepareMainSection(leveledTopographyData);

            // ISO 25178-71 file trailer
            bcr.PrepareTrailerSection(bcrMetaData);

            // now generate output
            string outFileName;

            if (fileNames.Length >= 2)
            {
                outFileName = fileNames[1];
            }
            else
            {
                outFileName = nmmFileName.GetFreeFileNameWithIndex("sdf");
            }

            ConsoleUI.WritingFile(outFileName);
            if (!bcr.WriteToFile(outFileName))
            {
                ConsoleUI.Abort();
                ConsoleUI.ErrorExit("!could not write file", 4);
            }
            ConsoleUI.Done();
        }
Beispiel #2
0
        public static void Main(string[] args)
        {
            Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
            if (!CommandLine.Parser.Default.ParseArgumentsStrict(args, options))
            {
                Console.WriteLine("*** ParseArgumentsStrict returned false");
            }
            // consume the verbosity option
            if (options.BeQuiet == true)
            {
                ConsoleUI.BeSilent();
            }
            else
            {
                ConsoleUI.BeVerbatim();
            }
            // print a welcome message
            ConsoleUI.Welcome();
            ConsoleUI.WriteLine();
            // get the filename(s)
            fileNames = options.ListOfFileNames.ToArray();
            if (fileNames.Length == 0)
            {
                ConsoleUI.ErrorExit("!Missing input file", 1);
            }

            // if no filetype was choosen, use default one
            if (!(options.convertBcr ||
                  options.convertPrDe ||
                  options.convertPrEn ||
                  options.convertPrf ||
                  options.convertSig ||
                  options.convertSmd ||
                  options.convertTxt ||
                  options.convertCsv ||
                  options.convertX3p))
            {
                options.convertSig = true;                      // this should be convertBcr in the future
            }
            // read all relevant scan data
            ConsoleUI.StartOperation("Reading and evaluating files");
            nmmFileNameObject = new NmmFileName(fileNames[0]);
            nmmFileNameObject.SetScanIndex(options.ScanIndex);
            theData = new NmmScanData(nmmFileNameObject);
            ConsoleUI.Done();
            ConsoleUI.WriteLine();

            if (options.DoHeydemann)
            {
                theData.ApplyHeydemannCorrection();
                if (theData.HeydemannCorrectionApplied)
                {
                    ConsoleUI.WriteLine($"Heydemann correction applied, span {theData.HeydemannCorrectionSpan * 1e9:F1} nm");
                }
                else
                {
                    ConsoleUI.WriteLine($"Heydemann correction not successful.");
                }
                ConsoleUI.WriteLine();
            }

            // some checks of the provided CLA options
            if (options.ProfileIndex < 0)
            {
                options.ProfileIndex = 0;   // automatically extract all profiles
            }
            if (options.ProfileIndex > theData.MetaData.NumberOfProfiles)
            {
                options.ProfileIndex = theData.MetaData.NumberOfProfiles;
            }

            topographyProcessType = TopographyProcessType.ForwardOnly;
            if (options.UseBack)
            {
                topographyProcessType = TopographyProcessType.BackwardOnly;
            }
            if (options.UseBoth)
            {
                topographyProcessType = TopographyProcessType.Average;
            }
            if (options.UseDiff)
            {
                topographyProcessType = TopographyProcessType.Difference;
            }
            if (theData.MetaData.ScanStatus == ScanDirectionStatus.ForwardOnly)
            {
                if (topographyProcessType != TopographyProcessType.ForwardOnly)
                {
                    ConsoleUI.WriteLine("No backward scan data present, switching to forward only.");
                }
                topographyProcessType = TopographyProcessType.ForwardOnly;
            }
            if (theData.MetaData.ScanStatus == ScanDirectionStatus.Unknown)
            {
                ConsoleUI.ErrorExit("!Unknown scan type", 2);
            }
            if (theData.MetaData.ScanStatus == ScanDirectionStatus.NoData)
            {
                ConsoleUI.ErrorExit("!No scan data present", 3);
            }

            // now we can start to sort and format everything we need

            prf.CreationDate         = theData.MetaData.CreationDate;
            prf.SampleIdentification = theData.MetaData.SampleIdentifier;
            prf.DeltaX      = theData.MetaData.ScanFieldDeltaX * 1e6;
            prf.UserComment = options.UserComment;

            // extract the requested profile
            if (options.ProfileIndex != 0)
            {
                ProcessSingleProfile(options.ProfileIndex);
                return;
            }
            // (ProfileIndex == 0) => extract all profiles
            for (int i = 1; i <= theData.MetaData.NumberOfProfiles; i++)
            {
                ProcessSingleProfile(i);
            }
        }
Beispiel #3
0
        public static void Main(string[] args)
        {
            CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

            // parse command line arguments
            if (!CommandLine.Parser.Default.ParseArgumentsStrict(args, options))
            {
                Console.WriteLine("*** ParseArgumentsStrict returned false");
            }
            // consume the verbosity option
            if (options.BeQuiet == true)
            {
                ConsoleUI.BeSilent();
            }
            else
            {
                ConsoleUI.BeVerbatim();
            }
            // print a welcome message
            ConsoleUI.Welcome();
            ConsoleUI.WriteLine();
            // get the filename(s)
            fileNames = options.ListOfFileNames.ToArray();
            if (fileNames.Length == 0)
            {
                ConsoleUI.ErrorExit("!Missing input file name", 1);
            }

            // read all relevant scan data
            ConsoleUI.StartOperation("Reading NMM scan files");
            nmmFileNameObject = new NmmFileName(fileNames[0]);
            nmmFileNameObject.SetScanIndex(options.ScanIndex);
            theData = new NmmScanData(nmmFileNameObject);
            ConsoleUI.Done();

            // check if data present
            if (theData.MetaData.ScanStatus == ScanDirectionStatus.Unknown)
            {
                ConsoleUI.ErrorExit("!Unknown scan type", 4);
            }
            if (theData.MetaData.ScanStatus == ScanDirectionStatus.NoData)
            {
                ConsoleUI.ErrorExit("!No scan data present", 5);
            }

            // Check if requested channels are present in raw data
            if (!theData.ColumnPresent(options.XAxisDesignation))
            {
                ConsoleUI.ErrorExit($"!Requested channel {options.XAxisDesignation} not in data files", 2);
            }
            if (!theData.ColumnPresent(options.ZAxisDesignation))
            {
                ConsoleUI.ErrorExit($"!Requested channel {options.ZAxisDesignation} not in data files", 3);
            }

            // one must avoid referencing to an line outside of expected number of line marks
            if (options.RefLine < 0)
            {
                options.RefLine = 0;
            }
            if (options.RefLine >= options.ExpectedTargets)
            {
                options.RefLine = options.ExpectedTargets - 1;
            }

            // output scan files peculiaritues
            ConsoleUI.WriteLine();
            ConsoleUI.WriteLine($"SpuriousDataLines: {theData.MetaData.SpuriousDataLines}");
            ConsoleUI.WriteLine($"NumberOfGlitchedDataPoints: {theData.MetaData.NumberOfGlitchedDataPoints}");

            // some screen output
            ConsoleUI.WriteLine();
            ConsoleUI.WriteLine($"{theData.MetaData.NumberOfDataPoints} data lines with {theData.MetaData.NumberOfColumnsInFile} channels, organized in {theData.MetaData.NumberOfProfiles} profiles");
            ConsoleUI.WriteLine($"x-axis channel: {options.XAxisDesignation}");
            ConsoleUI.WriteLine($"z-axis channel: {options.ZAxisDesignation}");
            ConsoleUI.WriteLine($"Threshold: {options.Threshold}");
            ConsoleUI.WriteLine($"Morphological filter parameter: {options.Morpho}");
            if (options.LineScale)
            {
                ConsoleUI.WriteLine($"Expected number of line marks: {options.ExpectedTargets}");
                ConsoleUI.WriteLine($"Nominal scale division: {options.NominalDivision} um");
            }
            ConsoleUI.WriteLine();

            // evaluate the intensities for ALL profiles == the whole scan field
            ConsoleUI.StartOperation("Classifying intensity data");
            double[]           luminanceField = theData.ExtractProfile(options.ZAxisDesignation, 0, TopographyProcessType.ForwardOnly);
            IntensityEvaluator eval           = new IntensityEvaluator(luminanceField);

            ConsoleUI.Done();
            ConsoleUI.WriteLine();
            ConsoleUI.WriteLine($"Intensity range from {eval.MinIntensity} to {eval.MaxIntensity}");
            ConsoleUI.WriteLine($"Estimated bounds from {eval.LowerBound} to {eval.UpperBound}");
            double relativeSpan = (double)(eval.UpperBound - eval.LowerBound) / (double)(eval.MaxIntensity - eval.MinIntensity) * 100.0;

            ConsoleUI.WriteLine($"({relativeSpan:F1} % of full range)");
            ConsoleUI.WriteLine();

            // prepare object for the overall dimensional result
            LineScale result = new LineScale(options.ExpectedTargets);

            result.SetNominalValues(options.NominalDivision, options.RefLine);

            // wraps profiles extracted from NMM files to a more abstract structure: profiles
            List <IntensityProfile> profilesList = new List <IntensityProfile>();

            WarpNmmProfiles(TopographyProcessType.ForwardOnly, profilesList);
            if (theData.MetaData.ScanStatus == ScanDirectionStatus.ForwardAndBackward || theData.MetaData.ScanStatus == ScanDirectionStatus.ForwardAndBackwardJustified)
            {
                WarpNmmProfiles(TopographyProcessType.BackwardOnly, profilesList);
            }
            IntensityProfile[] profiles = profilesList.ToArray();

            // the loop over all profiles
            for (int profileIndex = 0; profileIndex < profiles.Length; profileIndex++)
            {
                if (!profiles[profileIndex].IsValid)
                {
                    ConsoleUI.ErrorExit($"!Profile {profileIndex} invalid", 6);
                }
                Classifier   classifier = new Classifier(profiles[profileIndex].Zvalues);
                int[]        skeleton   = classifier.GetSegmentedProfile(options.Threshold, eval.LowerBound, eval.UpperBound);
                MorphoFilter filter     = new MorphoFilter(skeleton);
                skeleton = filter.FilterWithParameter(options.Morpho);
                LineDetector marks = new LineDetector(skeleton, profiles[profileIndex].Xvalues);
                if (options.LineScale)
                {
                    ConsoleUI.WriteLine($"profile: {profileIndex+1,3} with {marks.LineCount} line marks {(marks.LineCount != options.ExpectedTargets ? "*" : " ")}");
                }
                result.UpdateSample(marks.LineMarks, options.RefLine);
                if (options.EdgeOnly)
                {
                    ConsoleUI.WriteLine($"profile: {profileIndex+1,3} with L:{marks.LeftEdgePositions.Count} R:{marks.RightEdgePositions.Count}");
                    EdgesOnlyOutputToStringBuilder(marks, profileIndex);
                }
            }

            #region output
            // prepare output
            string        outFormater = $"F{options.Precision}";
            StringBuilder sb          = new StringBuilder();
            sb.AppendLine($"{ConsoleUI.WelcomeMessage}");
            sb.AppendLine($"InputFile            = {theData.MetaData.BaseFileName}");
            // section for sample specific metadata
            sb.AppendLine($"SampleIdentifier     = {theData.MetaData.SampleIdentifier}");
            sb.AppendLine($"SampleSpecies        = {theData.MetaData.SampleSpecies}");
            sb.AppendLine($"SampleSpecification  = {theData.MetaData.SampleSpecification}");
            if (options.LineScale)
            {
                sb.AppendLine($"ExpectedLineMarks    = {options.ExpectedTargets}");
                sb.AppendLine($"NominalDivision      = {options.NominalDivision} µm");
                sb.AppendLine($"ScaleType            = {result.ScaleType}");
            }
            sb.AppendLine($"ThermalExpansion     = {options.Alpha.ToString("E2")} 1/K");
            // scan file specific data
            sb.AppendLine($"NumberOfScans        = {theData.MetaData.NumberOfScans}");
            sb.AppendLine($"ScanIndex            = {theData.MetaData.ScanIndex}");
            sb.AppendLine($"PointsPerProfile     = {theData.MetaData.NumberOfDataPoints}");
            sb.AppendLine($"Profiles             = {theData.MetaData.NumberOfProfiles}");
            sb.AppendLine($"InputChannels        = {theData.MetaData.NumberOfColumnsInFile}");
            sb.AppendLine($"PointSpacing         = {(theData.MetaData.ScanFieldDeltaX * 1e6).ToString("F4")} µm");
            sb.AppendLine($"ProfileSpacing       = {(theData.MetaData.ScanFieldDeltaY * 1e6).ToString("F4")} µm");
            sb.AppendLine($"ScanFieldCenterX     = {theData.MetaData.ScanFieldCenterX * 1000:F1} mm");
            sb.AppendLine($"ScanFieldCenterY     = {theData.MetaData.ScanFieldCenterY * 1000:F1} mm");
            sb.AppendLine($"ScanFieldCenterZ     = {theData.MetaData.ScanFieldCenterZ * 1000:F1} mm");
            sb.AppendLine($"AngularOrientation   = {theData.MetaData.ScanFieldRotation:F2}°");
            sb.AppendLine($"ScanSpeed            = {theData.MetaData.ScanSpeed} µm/s");
            sb.AppendLine($"GlitchedDataPoints   = {theData.MetaData.NumberOfGlitchedDataPoints}");
            sb.AppendLine($"SpuriousDataLines    = {theData.MetaData.SpuriousDataLines}");
            sb.AppendLine($"Probe                = {theData.MetaData.ProbeDesignation}");
            // evaluation parameters, user supplied
            sb.AppendLine($"X-AxisChannel        = {options.XAxisDesignation}");
            sb.AppendLine($"Z-AxisChannel        = {options.ZAxisDesignation}");
            sb.AppendLine($"Threshold            = {options.Threshold}");
            sb.AppendLine($"FilterParameter      = {options.Morpho}");
            if (options.LineScale)
            {
                sb.AppendLine($"ReferencedToLine     = {options.RefLine}");
                double maximumThermalCorrection = ThermalCorrection(result.LineMarks.Last().NominalPosition) - ThermalCorrection(result.LineMarks.First().NominalPosition);
                sb.AppendLine($"MaxThermalCorrection = {maximumThermalCorrection:F3} µm");
            }
            // auxiliary values
            sb.AppendLine($"MinimumIntensity     = {eval.MinIntensity}");
            sb.AppendLine($"MaximumIntensity     = {eval.MaxIntensity}");
            sb.AppendLine($"LowerPlateau         = {eval.LowerBound}");
            sb.AppendLine($"UpperPlateau         = {eval.UpperBound}");
            sb.AppendLine($"RelativeSpan         = {relativeSpan:F1} %");
            if (options.LineScale)
            {
                sb.AppendLine($"EvaluatedProfiles    = {result.SampleSize}");
            }
            // environmental data
            sb.AppendLine($"SampleTemperature    = {theData.MetaData.SampleTemperature.ToString("F3")} °C");
            sb.AppendLine($"AirTemperature       = {theData.MetaData.AirTemperature.ToString("F3")} °C");
            sb.AppendLine($"AirPressure          = {theData.MetaData.BarometricPressure.ToString("F0")} Pa");
            sb.AppendLine($"AirHumidity          = {theData.MetaData.RelativeHumidity.ToString("F1")} %");
            sb.AppendLine("======================");
            if (options.LineScale)
            {
                sb.AppendLine("1 : Line number (tag)");
                sb.AppendLine("2 : Nominal value / µm");
                sb.AppendLine("3 : Position deviation / µm");
                sb.AppendLine("4 : StdDev of line position values / µm");
                sb.AppendLine("5 : Range of line position values / µm");
                sb.AppendLine("6 : Line width / µm");
                sb.AppendLine("7 : StdDev of line widths / µm");
                sb.AppendLine("8 : Range of line widths / µm");
                sb.AppendLine("@@@@");
                if (result.SampleSize == 0)
                {
                    sb.AppendLine("*** No matching intensity pattern found ***");
                }
                else
                {
                    foreach (var line in result.LineMarks)
                    {
                        double deltaL = ThermalCorrection(line.NominalPosition);
                        sb.AppendLine($"{line.Tag.ToString().PadLeft(5)}" +
                                      $"{line.NominalPosition.ToString("F0").PadLeft(10)}" +
                                      $"{(line.Deviation + deltaL).ToString(outFormater).PadLeft(10)}" +
                                      $"{line.LineCenterStdDev.ToString(outFormater).PadLeft(10)}" +
                                      $"{line.LineCenterRange.ToString(outFormater).PadLeft(10)}" +
                                      $"{line.AverageLineWidth.ToString(outFormater).PadLeft(10)}" +
                                      $"{line.LineWidthStdDev.ToString(outFormater).PadLeft(10)}" +
                                      $"{(line.LineWidthRange).ToString(outFormater).PadLeft(10)}");
                    }
                }
            }
            if (options.EdgeOnly)
            {
                sb.Append(edgesOnlyOutput);
            }
            #endregion

            #region File output
            string outFileName;
            if (fileNames.Length >= 2)
            {
                outFileName = fileNames[1]; // path and extension must be explicitely given
            }
            else
            {
                outFileName = nmmFileNameObject.GetFreeFileNameWithIndex(".prn"); // extension will be added by WriteToFile()
            }
            ConsoleUI.WriteLine();
            ConsoleUI.WritingFile(outFileName);
            StreamWriter hOutFile = File.CreateText(outFileName);
            hOutFile.Write(sb);
            hOutFile.Close();
            ConsoleUI.Done();
            #endregion
        }