/// <inheritdoc/>
        public void TraceInconstructibleTheorem(RankedTheorem rankedTheorem, AnalyticException exception)
        {
            // If logging is allowed, log it with the reference to more detail in the file
            if (_settings.LogFailures)
            {
                Log.Warning("Problem while drawing a ranked theorem. See {path} for more detail.", _settings.FailureFilePath);
            }

            // Open the stream writer for the file
            using var streamWriter = new StreamWriter(_settings.FailureFilePath, append: true);

            // Prepare the formatter
            var rankedTheoremFormatter = new OutputFormatter(rankedTheorem.Configuration.AllObjects);

            // Write initial info
            streamWriter.WriteLine($"Problem while constructing the theorem:\n");

            // Write the configuration
            streamWriter.WriteLine(rankedTheoremFormatter.FormatConfiguration(rankedTheorem.Configuration));

            // Write the theorem
            streamWriter.WriteLine($"\n{rankedTheoremFormatter.FormatTheorem(rankedTheorem.Theorem)}");

            // Write the exception
            streamWriter.WriteLine($"\nException: {exception}");

            // Separator
            streamWriter.WriteLine("--------------------------------------------------\n");
        }
        /// <summary>
        /// Converts given ranked theorems to a string.
        /// </summary>
        /// <param name="rankedTheorems">The ranked theorems to be converted.</param>
        /// <returns>The string representing the ranked theorems.</returns>
        private static string RankedTheoremsToString(IEnumerable <RankedTheorem> rankedTheorems)
        // Go through the theorems
        => rankedTheorems.Select((rankedTheorem, index) =>
        {
            // Prepare the formatter of the configuration
            var formatter = new OutputFormatter(rankedTheorem.Configuration.AllObjects);

            // Prepare the header
            var header = $"Theorem {index + 1}";

            // Prepare the result where the header is framed in dashes
            var result = $"{new string('-', header.Length)}\n{header}\n{new string('-', header.Length)}\n\n";

            // Add the configuration
            result += formatter.FormatConfiguration(rankedTheorem.Configuration);

            // Add the theorem
            result += $"\n\n{formatter.FormatTheorem(rankedTheorem.Theorem)}" +
                      // Add the total ranking
                      $" - total ranking {rankedTheorem.Ranking.TotalRanking.ToStringWithDecimalDot()}\n\n";

            // Add the ranking
            result += TheoremRankingToString(rankedTheorem.Ranking);

            // Finally return the result
            return(result);
        })
        // Make each on a separate line
        .ToJoinedString("\n\n");
        /// <summary>
        /// Converts an original <see cref="RankedTheorem"/> object into an intermediate object to be serialized.
        /// </summary>
        /// <param name="rankedTheorem">The object to be converted.</param>
        /// <returns>The result of the conversion.</returns>
        public static RankedTheoremIntermediate Convert(RankedTheorem rankedTheorem)
        {
            // Prepare the formatter for the configuration
            var formatter = new OutputFormatter(rankedTheorem.Configuration.AllObjects);

            // Format the configuration
            var configurationString = formatter.FormatConfiguration(rankedTheorem.Configuration)
                                      // Replace all curly braces that are not supported by the parser (and don't matter after all)
                                      .Replace("{", "").Replace("}", "");

            // Format the theorem
            var theoremString = formatter.FormatTheorem(rankedTheorem.Theorem)
                                // Replace all curly braces that are not supported by the parser (and don't matter after all)
                                .Replace("{", "").Replace("}", "");

            // Return the final object
            return(new RankedTheoremIntermediate(theoremString, rankedTheorem.Ranking, configurationString));
        }
Example #4
0
        /// <inheritdoc/>
        public void MarkInvalidInferrence(Configuration configuration, Theorem invalidConclusion, InferenceRule inferenceRule, Theorem[] negativeAssumptions, Theorem[] possitiveAssumptions)
        {
            // Prepare the file path for the rule with the name of the rule
            var filePath = Path.Combine(_settings.InvalidInferenceFolder, $"{inferenceRule.ToString().Replace(Path.DirectorySeparatorChar, '_')}.{_settings.FileExtension}");

            // If adding this inference would reach the maximal number of written inferences, we're done
            if (_invalidInferencesPerFile.GetValueOrDefault(filePath) + 1 > _settings.MaximalNumberOfInvalidInferencesPerFile)
            {
                return;
            }

            // Otherwise create or get the file in the invalid inference folder
            using var writer = new StreamWriter(filePath, append: true);

            // Prepare the formatter of the configuration
            var formatter = new OutputFormatter(configuration.AllObjects);

            // Write the configuration
            writer.WriteLine(formatter.FormatConfiguration(configuration));

            // An empty line
            writer.WriteLine();

            // Write the incorrect theorem
            writer.WriteLine($" {formatter.FormatTheorem(invalidConclusion)}");

            // Write its assumptions
            possitiveAssumptions.ForEach(assumption => writer.WriteLine($"  - {formatter.FormatTheorem(assumption)}"));

            // As well as negative ones
            negativeAssumptions.ForEach(assumption => writer.WriteLine($"  ! {formatter.FormatTheorem(assumption)}"));

            // Separator
            writer.WriteLine("--------------------------------------------------\n");

            // Mark that we've used this inference
            _invalidInferencesPerFile[filePath] = _invalidInferencesPerFile.GetValueOrDefault(filePath) + 1;
        }
        /// <summary>
        /// Traces that a given contextual picture couldn't be cloned and extended with the new object
        /// already drawn in pictures representing some configuration.
        /// </summary>
        /// <param name="previousContextualPicture">The contextual picture that was correct and failed to add the new object.</param>
        /// <param name="newConfigurationPictures">The pictures holding geometry data of the new object that was added.</param>
        /// <param name="exception">The inner inconsistency exception that caused the issue.</param>
        public void InconstructibleContextualPictureByCloning(ContextualPicture previousContextualPicture, PicturesOfConfiguration newConfigurationPictures, InconsistentPicturesException exception)
        {
            // Prepare the initial information string
            var infoString = $"Undrawable object into a contextual picture.";

            // If logging is allowed, log it with the reference to more detail in the file
            if (_settings.LogFailures)
            {
                Log.Warning("Object generation: {info} See {path} for more detail.", infoString, _settings.FailureFilePath);
            }

            // Prepare the formatter for the configuration
            var formatter = new OutputFormatter(newConfigurationPictures.Configuration.AllObjects);

            // Add the data about how the object can be drawn
            infoString += $"\n\nThe object is the last object of the following defining configuration:\n\n{formatter.FormatConfiguration(newConfigurationPictures.Configuration).Indent(2)}";

            // Add the exception
            infoString += $"\n\nThe details of the exception: {exception.Format(formatter)}\n";

            // Open the stream writer for the file
            using var streamWriter = new StreamWriter(_settings.FailureFilePath, append: true);

            // Write indented message to the file
            streamWriter.WriteLine($"- {infoString.Indent(3).TrimStart()}");
        }
Example #6
0
        /// <summary>
        /// The entry method of the application.
        /// </summary>
        /// <param name="arguments">The three arguments:
        /// <list type="number">
        /// <item>Path to the inference rule folder.</item>
        /// <item>The extension of the inference rule files.</item>
        /// <item>Path to the object introduction rule file.</item>
        /// </list>
        /// </param>
        private static async Task Main(string[] arguments)
        {
            #region Kernel preparation

            // Prepare the settings for the inference rule provider
            var inferenceRuleProviderSettings = new InferenceRuleProviderSettings(ruleFolderPath: arguments[0], fileExtension: arguments[1]);

            // Prepare the settings for the object introduction rule provider
            var objectIntroductionRuleProviderSettings = new ObjectIntroductionRuleProviderSettings(filePath: arguments[2]);

            // Prepare the kernel
            var kernel = Infrastructure.NinjectUtilities.CreateKernel()
                         // That constructors configurations
                         .AddConstructor()
                         // That can find theorems
                         .AddTheoremFinder(new TheoremFindingSettings
                                           (
                                               // Look for theorems of any type
                                               soughtTheoremTypes: Enum.GetValues(typeof(TheoremType)).Cast <TheoremType>()
                                               // Except for the EqualObjects that don't have a finder
                                               .Except(TheoremType.EqualObjects.ToEnumerable())
                                               // Enumerate
                                               .ToArray(),

                                               // Exclude in-picture tangencies
                                               new TangentCirclesTheoremFinderSettings(excludeTangencyInsidePicture: true),
                                               new LineTangentToCircleTheoremFinderSettings(excludeTangencyInsidePicture: true)
                                           ))
                         // That can prove theorems
                         .AddTheoremProver(new TheoremProvingSettings
                                           (
                                               // Use the provider to find the inference rules
                                               new InferenceRuleManagerData(await new InferenceRuleProvider.InferenceRuleProvider(inferenceRuleProviderSettings).GetInferenceRulesAsync()),

                                               // Use the provider to find the object introduction rules
                                               new ObjectIntroducerData(await new ObjectIntroductionRuleProvider.ObjectIntroductionRuleProvider(objectIntroductionRuleProviderSettings).GetObjectIntroductionRulesAsync()),

                                               // Setup the prover
                                               new TheoremProverSettings
                                               (
                                                   // We will be strict and don't assume simplifiable theorems
                                                   assumeThatSimplifiableTheoremsAreTrue: false,

                                                   // We will find trivial theorems for all objects
                                                   findTrivialTheoremsOnlyForLastObject: false
                                               )
                                           ));

            #endregion

            #region Tests

            // Take the tests
            new[]
            {
                PerpendicularBisectorsAreConcurrent(),
                IncenterAndTangentLine(),
                Midpoints(),
                Parallelogram(),
                HiddenExcenter(),
                HiddenMidpoint(),
                LineTangentToCircle(),
                ConcurrencyViaObjectIntroduction(),
                SimpleLineSegments()
            }
            // Perform each
            .ForEach(configuration =>
            {
                #region Finding theorems

                // Prepare 3 pictures in which the configuration is drawn
                var pictures = kernel.Get <IGeometryConstructor>().ConstructWithUniformLayout(configuration, numberOfPictures: 3).pictures;

                // Prepare a contextual picture
                var contextualPicture = new ContextualPicture(pictures);

                // Find all theorems
                var theorems = kernel.Get <ITheoremFinder>().FindAllTheorems(contextualPicture);

                #endregion

                #region Writing theorems

                // Prepare the formatter of all the output
                var formatter = new OutputFormatter(configuration.AllObjects);

                // Prepare a local function that converts given theorems to a string
                string TheoremString(IEnumerable <Theorem> theorems) =>
                // If there are no theorems
                theorems.IsEmpty()
                // Then return an indication of it
                        ? "nothing"
                // Otherwise format each theorem
                        : theorems.Select(formatter.FormatTheorem)
                // Order alphabetically
                .Ordered()
                // Add the index
                .Select((theoremString, index) => $"[{index + 1}] {theoremString}")
                // Make each on a separate line
                .ToJoinedString("\n");

                // Write the configuration and theorems
                Console.WriteLine($"\nConfiguration:\n\n{formatter.FormatConfiguration(configuration).Indent(2)}\n");
                Console.WriteLine($"Theorems:\n\n{TheoremString(theorems.AllObjects).Indent(2)}\n");

                #endregion

                #region Proving theorems

                // Prepare a timer
                var totalTime = new Stopwatch();

                // Start it
                totalTime.Start();

                // Perform the theorem finding with proofs, without any assumed theorems
                var proverOutput = kernel.Get <ITheoremProver>().ProveTheoremsAndConstructProofs(new TheoremMap(), theorems, contextualPicture);

                // Stop the timer
                totalTime.Stop();

                #endregion

                #region Writing results

                // Get the proofs
                var proofString = proverOutput
                                  // Sort by the statement
                                  .OrderBy(pair => formatter.FormatTheorem(pair.Key))
                                  // Format each
                                  .Select(pair => formatter.FormatTheoremProof(pair.Value))
                                  // Trim
                                  .Select(proofString => proofString.Trim())
                                  // Make an empty line between each
                                  .ToJoinedString("\n\n");

                // Write it
                Console.WriteLine(proofString);

                // Write the unproven theorems too
                Console.WriteLine($"\nUnproved:\n\n{TheoremString(theorems.AllObjects.Except(proverOutput.Keys)).Indent(2)}\n");

                // Report time
                Console.WriteLine($"Total time: {totalTime.ElapsedMilliseconds}");
                Console.WriteLine("----------------------------------------------");

                #endregion
            });

            #endregion
        }
        /// <inheritdoc/>
        public void Run(LoadedProblemGeneratorInput input)
        {
            #region Prepare readable writers

            // Prepare the name of readable output files
            var nameOfReadableFiles = $"{_settings.OutputFilePrefix}{input.Id}.{_settings.FileExtension}";

            // If we should write readable output without proofs
            using var readableOutputWithoutProofsWriter = _settings.WriteReadableOutputWithoutProofs
                                                          // Prepare the writer for it
                ? new StreamWriter(new FileStream(Path.Combine(_settings.ReadableOutputWithoutProofsFolder, nameOfReadableFiles), FileMode.Create, FileAccess.Write, FileShare.Read))
                                                          // Otherwise null
                : null;

            // If we should write readable output with proofs
            using var readableOutputWithProofsWriter = _settings.WriteReadableOutputWithProofs
                                                       // Prepare the writer for it
                ? new StreamWriter(new FileStream(Path.Combine(_settings.ReadableOutputWithProofsFolder, nameOfReadableFiles), FileMode.Create, FileAccess.Write, FileShare.Read))
                                                       // Otherwise null
                : null;

            // Local function that writes a line to both readable writers, if they are available
            void WriteLineToBothReadableWriters(string line = "")
            {
                // Write to the standard writer
                readableOutputWithoutProofsWriter?.WriteLine(line);

                // Write to the writer with proofs
                readableOutputWithProofsWriter?.WriteLine(line);
            }

            #endregion

            #region Prepare JSON writer

            // Prepare the name of the JSON output file
            var jsonOutputFileName = $"{_settings.OutputFilePrefix}{input.Id}.json";

            // If we should write the JSON output
            var jsonOutputWriter = _settings.WriteJsonOutput
                                   // Prepare the writer for it
                ? _factory.Create(Path.Combine(_settings.JsonOutputFolder, jsonOutputFileName))
                                   // Otherwise null
                : null;

            #endregion

            // Call the generation algorithm
            var(initialTheorems, outputs) = _generator.Generate(input);

            #region Write constructions

            // Write the constructions header
            WriteLineToBothReadableWriters($"Constructions:\n");

            // Write all of them
            input.Constructions.ForEach(construction => WriteLineToBothReadableWriters($" - {construction}"));

            // An empty line
            WriteLineToBothReadableWriters();

            #endregion

            #region Write the initial configuration

            // Prepare the formatter for the initial configuration
            var initialFormatter = new OutputFormatter(input.InitialConfiguration.AllObjects);

            // Write it
            WriteLineToBothReadableWriters("Initial configuration:\n");
            WriteLineToBothReadableWriters(initialFormatter.FormatConfiguration(input.InitialConfiguration));

            // Write its theorems, if there are any
            if (initialTheorems.Any())
            {
                WriteLineToBothReadableWriters("\nTheorems:\n");
                WriteLineToBothReadableWriters(InitialTheoremsToString(initialFormatter, initialTheorems));
            }

            #endregion

            #region Write other setup

            // Write iterations
            WriteLineToBothReadableWriters($"\nIterations: {input.NumberOfIterations}");

            // Write maximal numbers of objects of particular types
            WriteLineToBothReadableWriters($"{input.MaximalNumbersOfObjectsToAdd.Select(pair => $"MaximalNumberOf{pair.Key}s: {pair.Value}").ToJoinedString("\n")}\n");

            // Write whether we're excluding symmetry
            WriteLineToBothReadableWriters($"SymmetryGenerationMode: {input.SymmetryGenerationMode}");

            #endregion

            // Write results header
            WriteLineToBothReadableWriters($"Results:");

            // Log that we've started
            Log.Information("Generation has started.");

            #region Tracking variables

            // Prepare the number of generated configurations
            var numberOfGeneratedConfigurations = 0;

            // Prepare the total number of interesting theorems
            var numberOfInterestingTheorems = 0;

            // Prepare the total number of configurations with an interesting theorem
            var numberOfConfigurationsWithInterestingTheorem = 0;

            #endregion

            #region Start stopwatch

            // Prepare a stopwatch to measure the execution time
            var stopwatch = new Stopwatch();

            // Start it
            stopwatch.Start();

            #endregion

            // Begin writing of the JSON output file
            jsonOutputWriter?.BeginWriting();

            // Prepare the variable indicating whether we're writing best theorems,
            // which happens when we want to write them either readable or JSON form
            var writeBestTheorems = _settings.WriteReadableBestTheorems || _settings.WriteJsonBestTheorems;

            // Prepare the variable indicating the last time we rewrote the best theorems
            DateTimeOffset?lastTimeBestTheoremsWereRewritten = null;

            #region Generation loop

            // Run the generation
            foreach (var generatorOutput in outputs)
            {
                // Mark the configuration
                numberOfGeneratedConfigurations++;

                #region Logging progress

                // Find out if we should log progress and if yes, do it
                if (_settings.LogProgress && numberOfGeneratedConfigurations % _settings.ProgressLoggingFrequency == 0)
                {
                    // Calculate how long on average it takes to generate 'one batch' according to the progress frequency
                    var averageTime = (double)stopwatch.ElapsedMilliseconds / numberOfGeneratedConfigurations * _settings.ProgressLoggingFrequency;

                    // Based on whether we're creating best theorems, write how many of them we have
                    var bestTheoremString = $"{(writeBestTheorems ? $" ({_resolver.AllSorters.Select(pair => pair.sorter.BestTheorems.Count()).Sum()} after global merge)" : "")}";

                    // Log what we have
                    Log.Information("Generated configurations: {configurations}, after {time} ms, {frequency} in {averageTime:F2} ms on average, with {theorems} theorems{bestTheorems} " +
                                    "in {allConfigurations} configurations", numberOfGeneratedConfigurations, stopwatch.ElapsedMilliseconds, _settings.ProgressLoggingFrequency,
                                    averageTime, numberOfInterestingTheorems, bestTheoremString, numberOfConfigurationsWithInterestingTheorem);
                }

                #endregion

                // Skip configurations without theorems
                if (generatorOutput.NewTheorems.AllObjects.Count == 0)
                {
                    continue;
                }

                // Prepare the output of the analyzer
                GeneratedProblemAnalyzerOutputBase analyzerOutput;

                #region Analyzer call

                try
                {
                    // If we should look for proofs (because we should be writing them or analyze the inner inferences)
                    analyzerOutput = _settings.WriteInferenceRuleUsages || _settings.WriteReadableOutputWithProofs
                                     // Then call the analysis that construct them
                        ? (GeneratedProblemAnalyzerOutputBase)_analyzer.AnalyzeWithProofConstruction(generatorOutput, input.SymmetryGenerationMode)
                                     // Otherwise we don't need them
                        : _analyzer.AnalyzeWithoutProofConstruction(generatorOutput, input.SymmetryGenerationMode);
                }
                catch (Exception e)
                {
                    // If there is any sort of problem, we should make aware of it.
                    Log.Error(e, "There has been an exception while analyzing the configuration:\n\n{configuration}\n",
                              // Write the problematic configuration
                              new OutputFormatter(generatorOutput.Configuration.AllObjects).FormatConfiguration(generatorOutput.Configuration));

                    // And move on, we still might get something cool
                    continue;
                }

                #endregion

                // Count in interesting theorems
                numberOfInterestingTheorems += analyzerOutput.InterestingTheorems.Count;

                // If this is a configuration with an interesting theorem, count it in
                if (analyzerOutput.InterestingTheorems.Any())
                {
                    numberOfConfigurationsWithInterestingTheorem++;
                }

                // Write JSON output
                jsonOutputWriter?.Write(analyzerOutput.InterestingTheorems);

                #region Handling best theorems

                // If we are supposed to be handling best theorems, do so
                if (writeBestTheorems)
                {
                    // Take the interesting theorems
                    var theoremsToBeJudged = analyzerOutput.InterestingTheorems
                                             // Group by type
                                             .GroupBy(rankedTheorem => rankedTheorem.Theorem.Type);

                    try
                    {
                        // Prepare the set of sorters whose content changed
                        var updatedSorterTypes = new HashSet <TheoremType>();

                        // Mark all interesting theorems
                        analyzerOutput.InterestingTheorems
                        // Grouped by type
                        .GroupBy(rankedTheorem => rankedTheorem.Theorem.Type)
                        // Handle each group
                        .ForEach(group =>
                        {
                            // Let the sorter judge the theorems
                            _resolver.GetSorterForType(group.Key).AddTheorems(group, out var localBestTheoremChanged);

                            // If there is any local change, mark it
                            if (localBestTheoremChanged)
                            {
                                updatedSorterTypes.Add(group.Key);
                            }
                        });

                        // Find out if we should rewrite the best theorems, i.e. it must be allowed
                        var shouldWeRewriteBestTheorems = _settings.WriteBestTheoremsContinuously
                                                          // And either we haven't done it yet
                                                          && (lastTimeBestTheoremsWereRewritten == null ||
                                                          // Or the number of seconds that have passed since the last rewrote
                                                              DateTimeOffset.Now.ToUnixTimeSeconds() - lastTimeBestTheoremsWereRewritten.Value.ToUnixTimeSeconds()
                                                          // Is more than our specified interval
                                                              > _settings.BestTheoremsRewrittingIntervalInSeconds);

                        // If we should write best theorems continuously and it is time to it
                        if (shouldWeRewriteBestTheorems)
                        {
                            // Do it
                            RewriteBestTheorems(updatedSorterTypes);

                            // After the update where it was done last time
                            lastTimeBestTheoremsWereRewritten = DateTimeOffset.Now;
                        }
                    }
                    catch (Exception e)
                    {
                        // If there is any sort of problem, we should make aware of it.
                        Log.Error(e, "There has been an exception while sorting theorems of the configuration:\n\n{configuration}\n",
                                  // Write the problematic configuration
                                  new OutputFormatter(generatorOutput.Configuration.AllObjects).FormatConfiguration(generatorOutput.Configuration));
                    }
                }

                #endregion

                #region Human-readable output

                // Prepare a formatter for the generated configuration
                var formatter = new OutputFormatter(generatorOutput.Configuration.AllObjects);

                // Prepare the header so we can measure it
                var header = $"Configuration {numberOfGeneratedConfigurations}";

                // Construct the header with dashes
                var headerWithConfiguration = $"\n{new string('-', header.Length)}\n{header}\n{new string('-', header.Length)}\n\n" +
                                              // And the configuration
                                              $"{formatter.FormatConfiguration(generatorOutput.Configuration)}";

                #region Writing to the writer of readable output without proofs

                // If there is anything interesting to write
                if (analyzerOutput.InterestingTheorems.Any())
                {
                    // Write the header
                    readableOutputWithoutProofsWriter?.Write(headerWithConfiguration);

                    // Write the analysis results without proofs
                    readableOutputWithoutProofsWriter?.Write(AnalyzerOutputToString(formatter, analyzerOutput, writeProofs: false));

                    // Flush it
                    readableOutputWithProofsWriter?.Flush();
                }

                #endregion

                #region Writing to the writer of output with proofs

                // Write the header
                readableOutputWithProofsWriter?.Write(headerWithConfiguration);

                // Write the analysis results with proofs
                readableOutputWithProofsWriter?.Write(AnalyzerOutputToString(formatter, analyzerOutput, writeProofs: true));

                // Flush it
                readableOutputWithoutProofsWriter?.Flush();

                #endregion

                #endregion

                #region Inference rule usage statistics

                // If we should write inference rule usages
                if (_settings.WriteInferenceRuleUsages)
                {
                    // Mark the proofs
                    _tracker.MarkProofs(((GeneratedProblemAnalyzerOutputWithProofs)analyzerOutput).TheoremProofs.Values);

                    // Prepare the writer
                    using var inferenceRuleUsageWriter = new StreamWriter(new FileStream(_settings.InferenceRuleUsageFilePath, FileMode.Create, FileAccess.Write, FileShare.Read));

                    // Rewrite the stats file
                    inferenceRuleUsageWriter.Write(InferenceRuleUsagesToString());
                }

                #endregion
            }

            #endregion

            // Rewrite the best theorems after the generation is finished
            RewriteBestTheorems();

            // Prepare the string explaining the state after merge
            var afterMergeString = $"{(writeBestTheorems ? $"{_resolver.AllSorters.Select(pair => pair.sorter.BestTheorems.Count()).Sum()}" : "-")}";

            // Write end
            WriteLineToBothReadableWriters("\n------------------------------------------------");
            WriteLineToBothReadableWriters($"Generated configurations: {numberOfGeneratedConfigurations}");
            WriteLineToBothReadableWriters($"Configurations with an interesting theorem: {numberOfConfigurationsWithInterestingTheorem}");
            WriteLineToBothReadableWriters($"Interesting theorems: {numberOfInterestingTheorems}");
            WriteLineToBothReadableWriters($"Interesting theorems after global merge: {afterMergeString}");

            // Log these stats as well
            Log.Information("Generated configurations: {count}", numberOfGeneratedConfigurations);
            Log.Information("Configurations with an interesting theorem: {count}", numberOfConfigurationsWithInterestingTheorem);
            Log.Information("Interesting theorems: {count}", numberOfInterestingTheorems);
            Log.Information("Interesting theorems after global merge: {count}", afterMergeString);
            Log.Information("Run-time: {time} ms", stopwatch.ElapsedMilliseconds);

            // Close the JSON output writer
            jsonOutputWriter?.EndWriting();
        }